Skip to content

Latest commit

 

History

History
1215 lines (941 loc) · 40 KB

File metadata and controls

1215 lines (941 loc) · 40 KB
title Fortran Parser Reference
audience developers
prerequisites repository structure, parser architecture
related adding-a-fortran-construct.md, repository-structure.md
status maintained
publication draft

Fortran parser reference (wrapper-focused subset)

This document defines the currently supported parser subset, expected behavior, and practical usage from terminal and Python.

1) Supported features (comprehensive)

1.1 Source forms and preprocessing

  • Free-form Fortran: .f90, .f95, .f03, .f08
  • Fixed-form Fortran: .f, .for, .ftn
  • Free/fixed comment stripping
  • Continuation handling for both forms

1.2 Procedure parsing

  • subroutine headers
  • function headers
  • Header modifiers: pure, elemental, recursive
  • Function result(...) parsing (tolerant support for results(...))

1.3 Declaration/argument parsing

  • Intrinsic types: integer, real, complex, logical, character
  • Kind extraction from declaration specs (kind=...)
  • Attribute extraction:
    • intent(in|out|inout)
    • optional
    • value
    • allocatable
    • pointer
    • target
  • Array extraction:
    • dimension(...)
    • variable-level shape syntax (x(:), x(n))

1.4 Modules, imports, and project context

  • Module discovery
  • Module variable extraction
  • Shared specification-part parsing for module-like scopes (modules, submodules, programs, and block-data units), preserving original line numbers while skipping contained procedure bodies where they are not wrap-relevant
  • use extraction at module and procedure scope
  • Explicit use symbol mappings preserve imported source names and local target names for renamed imports
  • Propagation of module-level use imports into contained procedures
  • Folder/project parsing with dependency-aware ordering
  • Cross-file kind constant resolution (e.g., kinds modules)
  • Cached compile-time expression resolution for local/module parameters, module/program variable shapes, and character lengths

1.5 Derived type parsing

  • type :: ... end type and legacy type name ... end type discovery
  • Parameterized derived-type headers such as type :: buffer_type(k, n) and declarations such as type(buffer_type(real64, 4))
  • Type attributes (e.g., abstract)
  • Inheritance (extends(parent))
  • Field extraction including shape/pointer/allocatable
  • Type-bound procedures:
    • procedure ... :: ... bindings with attributes (e.g. pass(self), nopass)
    • generic ... :: name => target1, target2

1.6 Parser diagnostics and wrapper planning boundary

  • Parser diagnostics report source-level parse errors and unsupported parser constructs.
  • Parser JSON remains parse-only and does not contain wrapper-plan decisions or support diagnostics.
  • Wrapper builds complete policy from semantic IR and validate the resulting wrapper plan. Unsupported contracts report the owning plan path and completed-policy diagnostic.

2) Public API surface

Supported public API:

  • parse_fortran_file(source_or_path, filename=None, encoding="utf-8") -> FortranFile
  • parse_fortran_project(files, encoding="utf-8") -> FortranProject

Parser organization notes

prik/parsers/fortran/parser.py is now intentionally organized into clearly labeled sections and carries embedded implementation guidance. Start with the thin public wrappers at the bottom, then read the class from top to bottom:

  • Regex/constants, parser-wide type aliases, private unit dataclasses, and the compile-time resolver
  • FortranParser internals grouped by domain:
    • public parse entrypoints (parse_file, parse_project). The supported module-level API remains the wrappers listed above.
    • source-unit visitors for files, modules, submodules, programs, procedures, interfaces, derived types, and block data
    • recursive source-unit slicing (header, specification part, execution part, contains) with original line numbers preserved on each slice
    • shared declaration parsing for module variables, program/block-data variables, procedure arguments/results, and derived-type fields
    • _helper_* methods for scoped parsing, expression resolution, same-level duplicate checks, and shared specification-part collection
  • Thin module-level convenience wrappers that delegate to a shared parser instance

Parser methods carry focused docstrings, with examples where a grammar visitor or lexical helper is easier to understand from a concrete call.

The Fortran parser is now packaged under prik.parsers.fortran rather than a top-level parser package. The package includes its CLI module, lexer, JSON-compatible parse models, project parser, type resolver, and utility helpers. Public callers should use the stable top-level prik parser exports or prik.parsers.fortran package imports.

Implementation Inventory And Maintenance

This file is the single maintained Fortran parser reference. It replaces the older standalone implementation-reference document; parser feature inventory, testing workflow, and maintenance guard policy live here.

The implementation inventory is maintained across these surfaces:

  • prik/parsers/fortran/parser.py owns source slicing, declaration extraction, diagnostics, project ordering, dependency resolution, and compile-time expression resolution.
  • prik/parsers/fortran/models.py owns parse-only dataclasses and JSON-compatible parser facts.
  • prik/semantics/fortran2ir.py owns conversion from parser facts to semantic IR, including kind mapping, compile-time specialization, storage contracts, projection metadata, and wrapper-planning inputs.
  • tests/fortran/source_parsing/parsing/ covers parser contracts, source-unit slicing, diagnostics, project behavior, and fixture regressions.
  • tests/fortran/semantic_ir/semantics/ covers semantic conversion, datatype precision mapping, wrapper planning, .pyi emission, and compile-time specialization.

parse_file is the central orchestration path. It first slices the source into direct file-level units, then each class visitor parses only its own substring and recursively slices direct children. This is the key parser design: each Fortran grammar unit has a header, a specification region, optional execution region, and optional contains region. The differences between modules, programs, procedures, derived types, interfaces, and block data are expressed by small visitor decisions and grammar flags rather than separate whole-file parsing loops.

Nested unit boundaries and placement outside execution regions are checked even when they are not exported as wrapper metadata. Internal procedures inside a host procedure's contains block are structurally sliced, then their declarations and bodies are skipped. Once an execution boundary is detected, procedure bodies and standalone included execution fragments are intentionally skipped. Procedure-local interface blocks are still visited enough to type callback dummy arguments and to preserve interface metadata.

2.1 Recursive parser sketch

Small input:

module m
  integer, parameter :: n = 4
contains
  subroutine scale(x)
    real, intent(inout) :: x(n)
  end subroutine scale
end module m

The parser handles it in this order:

  1. parse_file preprocesses the source and calls _helper_slice_child_units at file scope. The result is one ModuleUnit carrying the module name, lines, and source locations.
  2. the shared ClassVisitor._visit dispatcher selects _visit_ModuleUnit.
  3. _visit_ModuleUnit creates a module _ParserScope, calls _helper_split_unit_parts, and sends only the module specification lines to _parse_specification_part.
  4. _parse_specification_part uses the shared declaration backend: _helper_parse_declaration_line parses integer, parameter :: n = 4, then _helper_push_declaration_to_scope appends the resulting parameter variable to FortranModule.variables.
  5. The module visitor recursively slices direct children from its substring. It finds one procedure unit, scale, and dispatches it to _visit_ProcedureUnit.
  6. _visit_ProcedureUnit creates a procedure _ParserScope, splits the procedure into header/specification/execution/contains, and visits only the specification part. The same declaration backend parses real, intent(inout) :: x(n) and pushes the metadata into the procedure argument symbol table.

Scope is always an explicit argument to the shared helpers. That is the reason two modules can each define type :: state without conflict, while two same-level module m declarations or two same-level contained procedures with the same name are rejected by _helper_validate_sibling_units.

End-name validation is strict for structural units whose names define exported scope boundaries, such as modules, submodules, programs, interfaces, and derived types. Procedure end-name mismatches are still tolerated while slicing third-party sources because some accepted fixture code contains copy/paste procedure end labels; the procedure is closed by unit kind so parsing can continue, and duplicate procedure names are validated at the sibling scope.

The only separate specification-line visitors are grammar-specific: module-like units share _parse_module_like_spec_line, procedures use _parse_procedure_spec_line for implicit, external, import, and local parameter handling, and derived types use _parse_type_spec_line for sequence, private, and type-bound declaration rules. All three still call the same declaration parser/pusher for actual declarations.

Most parser organization changes are structural, but behavior, model-schema, coverage, or fixture changes should be reflected in this reference.

Parameter constants expose both value and serialized symbolic_value when available. value is reserved for a literal/evaluated result after compile-time folding. If an initializer cannot be evaluated safely, such as selected_real_kind(...), value is None and symbolic_value preserves the original initializer for validation, debugging, downstream diagnostics, and JSON consumers.

Procedure-local parameters may be folded into argument shapes during procedure finalization. Module-level and use-associated parameters used in procedure argument shapes are kept symbolic in the signature (x(n) remains ["n"]) and are treated as valid scope references for policy completion. Module/program variable shapes and parameter values can be resolved through the compile-time resolver when enough information is available.

Reimplementation Guide For Another Parser

Use the Fortran parser as the reference for any source language with nested program units, scoped declarations, and a later semantic handoff. The details are Fortran-specific, but the parser architecture is reusable.

Recommended frontend responsibilities:

  • Keep one typed model layer for parse-only facts.
  • Keep one parser orchestration class with thin public wrappers.
  • Slice source into grammar units before parsing declarations.
  • Pass scope explicitly into shared helpers rather than using global mutable parser state for symbol resolution.
  • Parse only wrapper-relevant specification facts; skip executable bodies once they are outside the parser contract.
  • Preserve source locations and original line numbers through preprocessing and recursive slicing.
  • Emit parser diagnostics for malformed source, but leave wrappability policy to semantic policy completion.

The Fortran data flow is:

source path or source text
  -> compiler/native include preprocessing
  -> FortranParser.parse_file(...)
  -> source-unit slices with original line numbers
  -> scoped specification parsing
  -> FortranFile parser facts
  -> parse_fortran_project(...) dependency ordering and namespace resolution
  -> semantics.fortran2ir conversion
  -> policy completion, `.pyi`, and the implemented Fortran wrapper stages

The recursive parsing pattern is:

  1. Identify direct child units at the current grammar level.
  2. Split each child into header, specification part, execution part, and contains part where that language construct allows them.
  3. Parse declarations only from the specification part.
  4. Recurse only into direct children that are legal for the current unit kind.
  5. Validate sibling names and scope-local duplicate declarations.
  6. Finalize procedure arguments/results after local declarations and parameters are known.
  7. Resolve cross-file or imported compile-time facts only at project or semantic-conversion boundaries.

When adding another parser, keep these test layers separate:

  • parser unit tests for grammar slicing and declarations;
  • parser fixture tests for stable JSON/model output;
  • parser error fixture tests for fatal diagnostic contracts;
  • project tests for dependency ordering and cross-file resolution;
  • CLI tests for frontend selection, stage dispatch, output files, and debug behavior;
  • semantic conversion tests for parser-to-IR mapping;
  • .pyi tests for generated and edited interface round trips.

Executable references:

  • Fortran parser walkthrough: tests/fortran/source_parsing/parsing/test_developer_tutorial.py
  • Procedure/type parsing: tests/fortran/source_parsing/parsing/
  • Scope and project behavior: tests/fortran/modules/parsing/test_scope_handling.py and tests/fortran/modules/parsing/test_project_scope_models.py
  • Fortran fixture workflow: tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py
  • Shared CLI behavior: tests/fortran/command_line_interface/pipeline/
  • Fortran semantic handoff: tests/fortran/semantic_ir/semantics/

3) Terminal usage and expected outputs

3.1 Basic CLI invocation

python -m prik parse path/to/file.f90

Recognizable Fortran files can omit --language. Directories require explicit frontend selection:

python -m prik parse path/to/fortran_src --language fortran

Fortran directories are recursively scanned for .f, .for, .ftn, .f90, .f95, .f03, .f08.

The Fortran frontend rejects unsupported non-Fortran syntax before wrapper-focused parsing when it appears outside executable procedure/program bodies, which are intentionally not represented in the extracted interface.

The human-readable parse tree keeps scope variables compact by default as vars=N. Add --show-vars to print the variables, or --print-limit N to print only the first N items in each repeated section.

3.2 Human-readable output example

Input Fortran (tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90):

module m1
contains
subroutine add1(n, x)
  integer, intent(in) :: n
  real(kind=8), intent(inout), dimension(n) :: x
end subroutine add1
end module m1

Command:

python -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90

Expected output:

File: tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90
  Modules: 1
    - module m1 (vars=0, uses=0)
      Procedures: 1
        - subroutine add1(n:integer[0], x:real(8)[1])

The same command with --show-vars uses the variable-expanded report path. This fixture currently has no module variables to print, so the output remains compact:

python -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 --show-vars
File: tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90
  Modules: 1
    - module m1 (vars=0, uses=0)
      Procedures: 1
        - subroutine add1(n:integer[0], x:real(8)[1])

For large files:

python -m prik parse path/to/file.f90 --show-vars --print-limit 50

--print-limit applies independently to modules, submodules, programs, block data units, derived types, fields, procedures, and variables when variables are shown. Counts such as Procedures: 80 and Variables: 657 still show the full totals even when only the first N entries are printed.

Interpretation:

  • Parsed entities are counted per file.
  • Free procedures (outside modules) are shown in top-level Procedures.
  • Module-contained procedures are nested under each module.
  • Empty sections are omitted from the human-readable report.

More complex example:

Input Fortran (mixed_example.f90):

Command:

python -m prik mixed_example.f90
File: mixed_example.f90
  Procedures: 1
    - subroutine driver(n:integer[0])
  Modules: 2
    - module math_ops (vars=1, uses=1)
      Procedures: 2
        - subroutine saxpy(n:integer[0], a:real[0], x:real[1], y:real[1])
        - function dot(x:real[1], y:real[1])
    - module io_ops (vars=0, uses=0)
      Procedures: 1
        - subroutine dump(v:real[1])

3.3 JSON and semantic output

Print parser JSON:

python -m prik tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 --json

Write parser JSON:

python -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 --json --out report.json

Expected JSON layout:

  • Top-level object keyed by input path
  • Per-file payload with keys:
    • signatures
    • types
    • modules
    • submodules
    • programs
    • block_data

When prik parse --json applies compiler preprocessing, the per-file payload also contains preprocessing_recipe. The CLI applies compiler preprocessing for file-based parsing; compiler linemarkers remain accepted for provenance. The recipe records the exact compiler executable or adapter, argv, include paths, macro flags, standard, extra compiler arguments, working directory, include graph, source mappings, diagnostics, and optional macro metadata used to produce the parsed stdout stream.

Fortran CPP directives are handled by the configured compiler. Native Fortran include "file.inc" statements are then expanded recursively by the preprocessing layer before the single parser pass. Native INCLUDE is textual insertion into the current scope; it is not a use import from a separately compiled module. Include lookup is relative to the including file first, then the configured include directories, duplicate textual inclusion is preserved, and missing files or cycles produce INCLUDE_NOT_FOUND or INCLUDE_CYCLE diagnostics.

use import shape:

  • A renamed import such as use list_input, delete_input => delete_input_list records both sides:
"uses": {
  "list_input": [
    {
      "source": "delete_input_list",
      "target": "delete_input"
    }
  ]
}

3.4 Semantic and wrapper-plan output

Parser output and semantic IR are separate stages. Run parser inspection with:

python -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90

Build a wrapper with the default wrapper stage. If the completed plan cannot lower a contract, the build reports the precise plan owner and blocker:

python -m prik tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90

Parser JSON stays parse-only.

Semantic IR JSON uses the same output channels, but the per-file payload is the semantic model projection instead of raw parser output:

python -m prik semantics tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90

Generated .pyi text is printed with:

python -m prik generate --pyi tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90

3.5 Parse-error diagnostics and debug mode

When parsing fails, the CLI prints a compiler-style diagnostic to stderr and exits with status code 1. By default this output is intended for end users: it includes the source location, diagnostic code, message, source line, and caret context, but it does not include a Python traceback.

Example command:

python -m prik tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_argument_name.f90

Example diagnostic shape:

tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_argument_name.f90:1:1: error[PARSE_DUPLICATE_ARGUMENT]: Duplicate argument name 'x' in procedure 'dup'.
  |
1 | subroutine dup(x, y, x)
  | ^

ANSI color is enabled by default when available; no color flag is needed for normal use. To disable color explicitly, pass --no-color or set the standard NO_COLOR environment variable:

python -m prik bad.f90 --no-color
NO_COLOR=1 python -m prik bad.f90

For parser development, use --debug to re-raise FortranParseError and let Python print the full traceback showing where the error was raised internally:

python -m prik bad.f90 --debug

The same developer mode can be enabled with the environment variable FORTRAN_PARSER_DEBUG=1:

FORTRAN_PARSER_DEBUG=1 python -m prik bad.f90

In debug mode, the traceback's final exception message also includes a note: parser raised at ... line with the internal parser file, line, and function that created the diagnostic.

4) Python usage and expected outputs

4.1 Parse folder namespace

from prik import parse_fortran_project
from pathlib import Path

files = list(Path("tests/fortran/source_parsing/parsing/fixtures/general").glob("*.f90"))[:5]
project = parse_fortran_project(files)
print(len(project.files))
print(len(project.modules))

Expected behavior:

  • Recursively scans Fortran files.
  • Resolves dependencies and module imports across files.
  • Returns aggregate namespace parse output.

4.2 Parse single file and convert it to semantic IR

from pathlib import Path
from prik import parse_fortran_file
from semantics.fortran2ir import fortran_file_to_semantic_modules

p = Path("tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90")
code = p.read_text()

parsed = parse_fortran_file(code, filename=str(p))
modules = fortran_file_to_semantic_modules(parsed, standalone_module_name=p.stem)
print("procedures", len(parsed.procedures))
print("semantic modules", len(modules))

Expected behavior:

  • parsed is a FortranFile aggregate model with parsed units and symbols.
  • modules is the semantic IR projection used by .pyi printing and wrapper planning.

4.3 Structured argument specifications

Compatibility fields such as FortranArgument.shape, lbound, ubound, and kind remain serialized as strings/lists. For callers that need typed access, argument and variable models also expose structured helpers:

  • structured_shape returns a FortranShape containing parsed dimensions.
  • Slice-like dimensions such as 1:n:2 are represented as FortranSlice.
  • Whole-expression function calls such as lbound(x, 1) are represented as FortranFunctionCall.
  • kind_expression and value_expression parse kind and value strings using the same lightweight expression model.

Example:

arg.shape
# ["lbound(src, 2):ubound(src, 2)"]

dim = arg.structured_shape.dimensions[0]
dim.lower.name
# "lbound"
dim.upper.name
# "ubound"

5) Running tests

Run all tests:

PYTHONPATH=. pytest -q

Run parser-focused tests:

python -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 --language fortran --json
PYTHONPATH=. pytest -q tests/fortran/source_parsing/parsing/
PYTHONPATH=. pytest -q tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py
PYTHONPATH=. pytest -q tests/fortran/command_line_interface/pipeline/

Focused test files by implementation area:

  • Parser walkthrough and expected developer flow: tests/fortran/source_parsing/parsing/test_developer_tutorial.py
  • Procedure headers, declarations, derived types, interfaces, and type-bound procedures: tests/fortran/source_parsing/parsing/
  • Function header edge cases: tests/fortran/functions/parsing/test_function_headers.py
  • Scope handling and project namespace behavior: tests/fortran/modules/parsing/test_scope_handling.py and tests/fortran/modules/parsing/test_project_scope_models.py
  • Preprocessing, native includes, and execution-boundary skipping: tests/fortran/source_preprocessing/preprocessing/test_parser_boundaries.py
  • Parser diagnostics and fatal error contracts: tests/fortran/source_parsing/parsing/test_error_handling.py
  • Regression contracts: tests/fortran/source_parsing/parsing/
  • Public entrypoints: tests/fortran/source_parsing/parsing/test_public_entrypoints.py
  • Parser fixture goldens: tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py
  • Parser error fixture goldens: tests/fortran/source_parsing/parsing/test_error_fixture_suite.py
  • Parser JSON shape: tests/fortran/source_parsing/parsing/test_json_sanity.py
  • Cached Fortran compiler/type and intrinsic-storage probing: tests/fortran/data_types/probes/test_fortran_type_probes.py
  • Shared CLI behavior: tests/fortran/command_line_interface/pipeline/

When adding or changing a Fortran parser feature, add a focused parser test near the implementation concern first, then update fixture goldens only when the serialized parser contract intentionally changes.

Update golden JSON fixtures:

python tests/fortran/source_parsing/parsing/generate_parser_goldens.py

Update selected fixture(s):

python tests/fortran/source_parsing/parsing/generate_parser_goldens.py tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90

In-test auto-update mode:

FORTRAN_PARSER_UPDATE_GOLDENS=1 PYTHONPATH=. pytest -q tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py --confcutdir=tests/

Semantic and .pyi fixtures have separate generators:

python tests/fortran/semantic_ir/semantics/generate_semantic_fixtures.py
WRAPPER_UPDATE_PYI_FIXTURES=1 python3 -m pytest -q tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py

6) Error handling

All parse failures raise FortranParseError, a subclass of ValueError. The exception keeps structured metadata for consumers:

  • filename — source path supplied to the parser, if any
  • line_number — 1-based source line where the error was detected, if known
  • source_line — original source text for context, if known
  • base_message — stable error text without location/source context
  • code — stable, explicit diagnostic category identifier; manually constructed fallback errors use PARSE_ERROR, while grammar rejection uses PARSE_INVALID_SYNTAX

Diagnostic codes are for programmatic matching in tests, tools, and documentation. The category name states the failure class directly. The shared registry is diagnostic-codes.md.

str(error) and error.format_diagnostic(color=False) render a compiler-style diagnostic:

<filename>:<line>:1: error[<CATEGORY>]: <message>
  |
<N> | <source line>
  | ^

If no filename is available, the location is rendered as <unknown>. If a line number or source line is unavailable, that part of the diagnostic is omitted or shown with ? as appropriate. Use error.base_message when tests or API consumers need only the message text.

format_diagnostic(color=True) adds ANSI styling. The CLI requests colored diagnostics by default when available; pass --no-color or set NO_COLOR=1 to disable ANSI output. On Windows, ANSI console compatibility is enabled through colorama when it is installed.

For parser development, format_diagnostic(debug=True) appends a note with the internal parser file, line, and function that raised the error. The CLI exposes this through --debug or FORTRAN_PARSER_DEBUG=1; normal CLI parse errors intentionally hide Python tracebacks.

The sections below list each error category, the triggering condition, and the exact base_message format (with <...> placeholders for runtime values).

6.1 Unknown or unsupported type declaration

Triggered when a declaration line cannot be matched to any known intrinsic type, type(...), or character variant.

In a procedure:

Unknown or unsupported datatype declaration for procedure '<name>': <line>

Example Fortran that triggers this:

subroutine bad(x)
  weirdtype :: x
end subroutine bad

Example error:

bad.f90:2:1: error[PARSE_UNSUPPORTED_DECLARATION]: Unknown or unsupported datatype declaration for procedure 'bad': weirdtype :: x
  |
2 |   weirdtype :: x
  | ^

In a derived type:

Unknown or unsupported datatype declaration in type '<name>': <line>

In a module:

Unknown or unsupported datatype declaration in module '<name>': <line>

6.2 Duplicate declaration

Triggered when the same symbol is declared more than once in the same scope.

In a procedure (arguments and local declarations):

Duplicate declaration of symbol '<name>' in procedure '<proc>'.

Example:

subroutine dup(x)
  real :: x
  integer :: x
end subroutine dup

Example error:

dup.f90:3:1: error[PARSE_DUPLICATE_DECLARATION]: Duplicate declaration of symbol 'x' in procedure 'dup'.
  |
3 |   integer :: x
  | ^

PARAMETER constants:

Duplicate PARAMETER declaration of symbol '<name>' in procedure '<proc>'.

In a derived type:

Duplicate field '<name>' in derived type '<type>'.

In a module:

Duplicate variable '<name>' in module '<module>'.

6.3 Duplicate procedure name

Triggered when the same procedure name appears more than once within the same module or global scope. Internal procedures inside separate host contains blocks are scoped to their host and do not conflict with each other.

Global scope:

Duplicate procedure name '<name>' in global scope.

Module scope:

Duplicate procedure name '<name>' in module '<module>'.

Example:

subroutine work(n)
  integer, intent(in) :: n
end subroutine work

subroutine work(n)
  integer, intent(in) :: n
end subroutine work

Example error:

dup.f90:5:1: error[PARSE_DUPLICATE_PROCEDURE]: Duplicate procedure name 'work' in global scope.
  |
5 | subroutine work(n)
  | ^

6.4 Duplicate argument name

Triggered when a procedure's argument list contains the same name more than once.

Duplicate argument name '<name>' in procedure '<proc>'.

Example:

subroutine dup(x, y, x)
  integer, intent(in) :: x
  real, intent(in) :: y
end subroutine dup

Example error:

dup_arg.f90:1:1: error[PARSE_DUPLICATE_ARGUMENT]: Duplicate argument name 'x' in procedure 'dup'.
  |
1 | subroutine dup(x, y, x)
  | ^

6.5 Star-kind declarations

Legacy type*N declarations, such as real*8, are accepted in both fixed-form and modern-extension files. Numeric star declarations preserve their fixed total storage width for semantic conversion. This matters most for complex types: complex*8 is an 8-byte Complex64, while modern complex(kind=8) is a compiler kind and is 16 bytes on the documented gfortran target. DOUBLE PRECISION and DOUBLE COMPLEX retain a compiler-dependent double-kind expression and use the cached Fortran type probe. For CHARACTER*N and CHARACTER*(*), the star value is a length, not a kind or element storage width.

subroutine accepted(x)
  real*8 :: x
end subroutine accepted

See the generated modern and legacy datatype mapping for the exact GitHub Actions target results.

6.6 Source-form metadata

The parser records source-form metadata from the filename and lexer, but does not reject a construct solely because a .f77 suffix was used. Grammar-region validation still applies after preprocessing.

6.7 Implicit none — undeclared argument or result

Triggered when implicit none is active and an argument (or function result) has no matching type declaration.

Argument:

Argument '<name>' in procedure '<proc>' has no type declaration (implicit none is active).

Function result:

Function result '<name>' in procedure '<proc>' has no type declaration (implicit none is active).

Example:

subroutine foo(x, y)
  implicit none
  integer, intent(in) :: x
end subroutine foo

Example error:

implicit_none.f90:1:1: error[PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL]: Argument 'y' in procedure 'foo' has no type declaration (implicit none is active).
  |
1 | subroutine foo(x, y)
  | ^

6.8 Unknown datatype for function result

Triggered when a function result has no resolvable type after parsing (and implicit none prevents implicit typing).

Unknown datatype for function result '<name>' in procedure '<proc>'.

Example:

function f(x) result(res)
  implicit none
  real :: x
end function f

Example error:

bad.f90:1:1: error[PARSE_UNKNOWN_FUNCTION_RESULT_TYPE]: Unknown datatype for function result 'res' in procedure 'f'.
  |
1 | function f(x) result(res)
  | ^

6.9 Unknown datatype for a module variable

Triggered by _validate_module_variables when a parsed module variable still has base_type == "unknown" after declaration parsing.

Unknown type for variable '<name>' in module '<module>'.

6.10 Unknown datatype for a derived type field

Triggered by _validate_derived_type_fields when a field still has base_type == "unknown".

Unknown type for field '<name>' in derived type '<type>'.

6.11 PARAMETER symbol without type in implicit none scope

Triggered when a legacy PARAMETER (...) statement names a symbol that has not been typed and implicit none is in effect.

Unknown datatype for PARAMETER symbol '<name>' in procedure '<proc>'.

Example:

      subroutine cst(a)
      implicit none
      real a
      parameter ( zero = 0.0e+0 )
      end

Example error:

legacy.f:4:1: error[PARSE_UNKNOWN_PARAMETER_TYPE]: Unknown datatype for PARAMETER symbol 'zero' in procedure 'cst'.
  |
4 |       parameter ( zero = 0.0e+0 )
  | ^

6.12 Function result variable shadows an argument

Triggered when a result(name) clause reuses an argument name (and the two names are different from each other — the special case result(f) on a function named f is allowed).

Function result variable '<result>' in function '<func>' shadows an argument name.

Example:

function f(res) result(res)
  integer, intent(in) :: res
end function f

Example error:

shadow.f90:1:1: error[PARSE_RESULT_SHADOWS_ARGUMENT]: Function result variable 'res' in function 'f' shadows an argument name.
  |
1 | function f(res) result(res)
  | ^

6.13 Failed to resolve declared argument

An internal safety check: if a symbol was explicitly declared but its type could not be applied (a parser regression guard), the following error is raised.

Failed to resolve declared argument '<name>' in procedure '<proc>'.

7) Scope note

This parser is intentionally wrapper-focused and not a complete Fortran front end. Unsupported syntax should be surfaced through parser diagnostics or later semantic policy inputs for incremental parser extension.

External callback dummy declarations

The parser accepts legacy callback-style declarations inside procedure scopes, including:

  • external :: cb (treated as a procedure-typed dummy)
  • real, external :: f / integer, external :: g (typed external function dummies)

Under implicit none, these declarations count as valid argument declarations, so callback arguments are not reported as missing datatype declarations.

8) File, project, and semantic entrypoints

Use the stable top-level API:

  • parse_fortran_file(source_or_path, filename=None, encoding="utf-8") -> FortranFile
  • parse_fortran_project(files, encoding="utf-8") -> FortranProject

Lower-level unit parsers are internal FortranParser methods.

Semantic conversion lives in prik/semantics/fortran2ir.py. It accepts parsed FortranFile (or selected FortranModule) structures and converts metadata into semantic IR consumed by the .pyi printer and current Fortran wrapper/runtime stages. Compiler-backed shared-CLI semantic stages resolve compiler-dependent kind expressions, measure numeric and logical intrinsic storage with storage_size, attach those facts to semantic types, and reuse memory and persistent caches. Character declarations are excluded from storage probing: their semantic type is String, while fixed or deferred element length is carried separately from the declaration or runtime descriptor. The generated mapping report describes the modeled eight-bit character code unit directly and does not manufacture a compiler probe fact for character rows. For the maintained GitHub Actions gfortran profile, unqualified integer, real, and complex map to Int32, Float32, and Complex64; target-changing flags can change those mappings. Source-driven wrapper builds add the normalized native Fortran compiler flags to the internal probe configuration, so semantic type facts and native implementation compilation use the same default-kind profile. The generated target datatype mapping measures and verifies those storage facts.

The Fortran probe cache key includes the generated expression source, resolved compiler binary identity, target flags, includes, macros, requested standard, working directory, target-related environment, and runner. The persistent location is $XDG_CACHE_HOME/prik/fortran_type_probe or ~/.cache/prik/fortran_type_probe; PRIK_CACHE_DIR changes the internal cache root. The standalone prik probe command additionally exposes --cache-dir and --refresh for explicit inspection runs.

The standalone probe can create a reusable report containing the exact compile-time and storage expressions needed by a source:

python3 -m prik probe --language fortran --compiler gfortran \
  --expr='selected_real_kind(12)' \
  --expr='storage_size(real(0.0,kind=8))' \
  --out build/fortran-types.json

The report is an inspection and verification output. Semantic conversion and wrapper builds measure the facts they need internally from their selected compiler; the report is not a second semantic-stage input path. A missing required expression is reported explicitly instead of falling back to an unrelated target mapping.

The semantic converter also supports compile-time specialization for values the parser intentionally leaves symbolic. Use collect_semantic_compile_time_requirements(parsed) to list missing parameter or kind values, then pass a dictionary such as {"selected_real_kind(12)": 8} to fortran_module_to_semantic_module(..., compile_time_values=...) or fortran_file_to_semantic_modules(..., compile_time_values=...). Existing semantic IR can be copied and specialized with resolve_semantic_compile_time_values(module, {"n": 64}).