[flang] Intrinsic accessibility depends on compilation layout
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
**Update (2026-09-10):** My current reading favors accepting the downstream procedure whether the modules are compiled together or separately. `implicit_intrinsic_facade.mod` should expose `max`. [The analysis](https://gist.github.com/MattPD/a5f1ef2a125fa2062283a721feb4060e) covers the standards evidence and implementation options.
**Original report:**
### Summary
Flang serializes implicitly recognized intrinsic procedures as explicit `INTRINSIC` declarations in module files. When Flang later reads such a module file, use association can expose those names.
A module containing only a USE statement makes a different set of names accessible depending on whether Flang compiles both modules from one source file or reads the used module from a generated module file.
I reproduced the acceptance difference with Compiler Explorer's Flang trunk build at upstream commit [`2e3d50b4111f324fb615c3c18d02189caa1ef62d`](https://github.com/llvm/llvm-project/commit/2e3d50b4111f324fb615c3c18d02189caa1ef62d). The module-file contents shown below come from a local build based on upstream `main` commit [`0fcc159d32cf4b26204cec573d015e9040428a63`](https://github.com/llvm/llvm-project/commit/0fcc159d32cf4b26204cec573d015e9040428a63). The local patch did not modify `flang/lib/Semantics/mod-file.cpp` or `flang/lib/Semantics/resolve-names.cpp`.
### Reproducer
`combined.f90`:
```fortran
module implicit_intrinsic_source
integer, parameter :: value = max(1, 2)
end module
module implicit_intrinsic_facade
use implicit_intrinsic_source
end module
```
`producer.f90`:
```fortran
module implicit_intrinsic_source
integer, parameter :: value = max(1, 2)
end module
```
`facade.f90`:
```fortran
module implicit_intrinsic_facade
use implicit_intrinsic_source
end module
```
`downstream.f90`:
```fortran
integer function imported_max(a, b)
use implicit_intrinsic_facade, only : max
integer, intent(in) :: a, b
imported_max = max(a, b)
end function
```
```console
mkdir -p combined separate
flang -fsyntax-only -module-dir combined combined.f90
flang -fsyntax-only -module-dir separate producer.f90
flang -fsyntax-only -I separate -module-dir separate facade.f90
flang -fsyntax-only -I combined downstream.f90
# error: 'max' not found in module 'implicit_intrinsic_facade'
# this diagnostic is correct: The module has no public name 'max'.
flang -fsyntax-only -I separate downstream.f90
# incorrectly accepted
```
Compiling `producer.f90` writes this `implicit_intrinsic_source.mod` body, with header lines omitted:
```fortran
module implicit_intrinsic_source
integer(4),parameter::value=2_4
intrinsic::max
end
```
The combined compilation writes this `implicit_intrinsic_facade.mod` body, with header lines omitted:
```fortran
module implicit_intrinsic_facade
use implicit_intrinsic_source,only:value
end
```
The separate compilation writes this `implicit_intrinsic_facade.mod` body, which includes an extra `only:max` line:
```fortran
module implicit_intrinsic_facade
use implicit_intrinsic_source,only:value
use implicit_intrinsic_source,only:max
end
```
### Expected behavior
Compilation layout should not change the set of names that a module makes accessible. A reference that the compiler resolves to the intrinsic `MAX` without an `INTRINSIC` statement should not make `max` an accessible name of the module.
### Cross-compiler results
I tested both layouts with five compilers. The combined layout compiles both modules from one source file. The separate layout compiles the facade against the producer's generated module file. Each Compiler Explorer link contains both layouts. The combined layout comes first.
- [Flang trunk 24.0.0git](https://flang.godbolt.org/z/vas56Esnh): rejects the combined layout and accepts the separate layout.
- [gfortran trunk 17.0.0](https://gfortran.godbolt.org/z/G1v1z9zdG): rejects both layouts.
- [ifx 2025.3.2](https://fortran.godbolt.org/z/KcPorTr98): accepts both layouts.
- [nvfortran 26.5](https://fortran.godbolt.org/z/adcjorzM8): rejects both layouts.
- Cray Fortran 19.0.0, tested locally: accepts both layouts.
Among these five compilers, only Flang's result differs between the two layouts. Both gfortran and nvfortran reject each layout, matching the expected result.
Both ifx and Cray expose the implicitly recognized `MAX` in each layout. A control test importing `ONLY: max` from an empty module fails on both compilers, so neither compiler accepts `MAX` in every `ONLY` list. Their consistent acceptance shows a related accessibility difference, not the module-file asymmetry reported here.
### Root cause
`ScopeHandler::AcquireIntrinsicProcedureFlags()` sets `Attr::INTRINSIC` in the symbol's `attrs()` and records the same flag in the symbol's `implicitAttrs()`.
`ModFileWriter::PutProcEntity()` tests `attrs()` and never consults `implicitAttrs()`, so it writes:
```fortran
intrinsic::max
```
When Flang reads the module file back, it parses that line as an explicit `INTRINSIC` statement. An implicit recognition therefore reappears as an explicit specification.
[F2023 15.5.5.2](https://j3-fortran.org/doc/year/23/23-007r1.pdf) separates two cases. The first case covers an intrinsic made accessible by an explicit `INTRINSIC` specification, or by use association from a module where the corresponding name has that specification. The second case covers a name recognized as an intrinsic procedure without such a specification.
Serializing implicit recognition as an explicit declaration can therefore affect generic resolution and module accessibility. The reproducer above shows only the accessibility effect.
### Related work
A general fix for the OpenMP problem reported in https://github.com/llvm/llvm-project/issues/209853 must distinguish an explicit source statement from a serialized implicit recognition.
The generic merging work in https://github.com/llvm/llvm-project/issues/191870 and https://github.com/llvm/llvm-project/pull/217005 covers only a single compilation. Neither carries the distinction between explicit and implicit through a module file.
### Open questions
Which direction would be preferable?
- Change `ModFileWriter` to omit intrinsic procedure symbols whose `INTRINSIC` flag is only implicit.
- Add module-file metadata that records whether the source declared each serialized intrinsic procedure explicitly.
If module metadata is preferable, should I propose the format on LLVM Discourse before implementing it?
Contributor guide
Assessment
This issue has not been assessed yet.