From 4dd46b06c3f0ed3aba1d74655bde9a3d16ac17ce Mon Sep 17 00:00:00 2001 From: said Date: Tue, 19 May 2026 09:38:00 +0100 Subject: [PATCH 1/2] improve parser --- fortran_parser/parser.py | 1428 ++++++++++++----- .../scifortran/SF_SPARSE_ARRAY_CSC.json | 34 +- .../scifortran/SF_SPARSE_ARRAY_CSR.json | 34 +- .../fixtures/scifortran/SF_SPARSE_COMMON.json | 8 +- 4 files changed, 1115 insertions(+), 389 deletions(-) diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index e43d7ed16..e03ca498e 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -4,7 +4,7 @@ import re import ast from pathlib import Path -from dataclasses import replace +from dataclasses import dataclass, replace from .lexer import preprocess_lines from .models import FortranArgument, FortranBlockData, FortranDerivedType, FortranFile, FortranInterface, FortranModule, FortranParseError, FortranProcedureSignature, FortranProgram, FortranProject, FortranSubmodule, FortranUseMapping, FortranVariable @@ -77,7 +77,7 @@ re.IGNORECASE, ) _LEGACY_PARAM_STMT_RE = re.compile(r"^parameter\s*\(\s*(?P.*)\s*\)$", re.IGNORECASE) -_DERIVED_TYPE_RE = re.compile(r"^type\s*(?P(?:,\s*[^:]+)?)::\s*(?P\w+)$", re.IGNORECASE) +_DERIVED_TYPE_RE = re.compile(r"^type\s*(?P(?:,\s*[^:]+)?)::\s*(?P\w+)(?:\s*\([^)]*\))?$", re.IGNORECASE) _TYPE_FIELD_RE = re.compile(r"^type\s*\(\s*(?P\w+)\s*\)\s*(?P.*)$", re.IGNORECASE) _CLASS_FIELD_RE = re.compile(r"^class\s*\(\s*(?P\w+)\s*\)\s*(?P.*)$", re.IGNORECASE) _PROC_BIND_RE = re.compile(r"^procedure\s*(?:,\s*[^:]*)?::\s*(?P.*)$", re.IGNORECASE) @@ -112,6 +112,43 @@ _SourceOrLines = str | _PreprocessedLines +@dataclass(frozen=True) +class _SourceUnit: + kind: str + name: str | None + lines: _PreprocessedLines + start_line: int | None + end_line: int | None + + +@dataclass +class _ParserScope: + kind: str + name: str | None + model: object | None = None + parent: "_ParserScope | None" = None + module_owner: str | None = None + state: dict | None = None + + +@dataclass(frozen=True) +class _UnitGrammar: + kind: str + has_execution_part: bool = False + has_contains_part: bool = False + ignores_contains_children: bool = False + declaration_role: str | None = None + + +@dataclass(frozen=True) +class _UnitParts: + header: tuple[str, int | None, str | None] | None + specification: _PreprocessedLines + execution: _PreprocessedLines + contains: _PreprocessedLines + footer: tuple[str, int | None, str | None] | None + + # ----------------------------------------------------------------------------- # Low-level syntax and declaration helpers # ----------------------------------------------------------------------------- @@ -1601,6 +1638,369 @@ def _is_module_program_unit_start(line: str) -> bool: def _is_contains_transition(line: str) -> bool: return line.lower() == "contains" + def _helper_unit_grammar(self, kind: str) -> _UnitGrammar: + """Return the grammar profile used by the small `visit_*_unit` methods. + + The name is intentionally explicit: callers ask for the grammar of a + source unit before splitting its body into the same high-level regions + that appear in the Fortran standard: header, specification part, + execution part, and contains part. + + Example: + A procedure has a specification part and an execution part, but + wrapper metadata only needs the specification part:: + + grammar = self._helper_unit_grammar("procedure") + assert grammar.has_execution_part is True + assert grammar.ignores_contains_children is True + """ + grammars = { + "module": _UnitGrammar( + kind="module", + has_contains_part=True, + declaration_role="module_variable", + ), + "submodule": _UnitGrammar( + kind="submodule", + has_contains_part=True, + declaration_role="module_variable", + ), + "program": _UnitGrammar( + kind="program", + has_execution_part=True, + has_contains_part=True, + ignores_contains_children=True, + declaration_role="module_variable", + ), + "procedure": _UnitGrammar( + kind="procedure", + has_execution_part=True, + has_contains_part=True, + ignores_contains_children=True, + declaration_role="procedure_symbol", + ), + "derived_type": _UnitGrammar( + kind="derived_type", + has_contains_part=True, + declaration_role="type_field", + ), + "interface": _UnitGrammar(kind="interface", has_contains_part=True), + "block_data": _UnitGrammar(kind="block_data", declaration_role="module_variable"), + "file": _UnitGrammar(kind="file", has_contains_part=True), + } + return grammars.get(kind, _UnitGrammar(kind=kind)) + + def _helper_classify_unit_start(self, line: str) -> tuple[str, str | None] | None: + """Classify a line that opens a parseable Fortran source unit. + + The helper name uses "classify" because it does not parse the unit; it + only recognizes the header enough for the slicer and visitors to agree + on what visitor should be called next. + + Example: + ``module mesh`` becomes ``("module", "mesh")`` while + ``module procedure reset`` becomes ``("procedure", "reset")``. + That lets a submodule `contains` region reuse the normal procedure + visitor instead of having a special submodule-only loop. + """ + stripped = line.strip() + if not stripped: + return None + lower = stripped.lower() + if lower.startswith("end "): + return None + submodule = _SUBMODULE_RE.match(stripped) + if submodule: + return "submodule", submodule.group("name") + module = _MODULE_RE.match(stripped) + if module: + return "module", module.group("name") + program = _PROGRAM_RE.match(stripped) + if program: + return "program", program.group("name") + block_data = _BLOCK_DATA_RE.match(stripped) + if block_data: + return "block_data", block_data.group("name") + starts_interface, interface_name = self._parse_interface_header(stripped) + if starts_interface: + return "interface", interface_name + module_proc = _MOD_PROC_IMPL_RE.match(stripped) + if module_proc: + return "procedure", module_proc.group("name") + proc_match = _PROC_RE.match(stripped) or _FUNC_RE.match(stripped) + if proc_match: + return "procedure", proc_match.group("name") + parsed_type = self._parse_derived_type_start(stripped) + if parsed_type: + return "derived_type", parsed_type[0] + return None + + @staticmethod + def _helper_unit_end_matches(kind: str, line: str) -> bool: + """Return whether `line` closes a unit of `kind`. + + The method isolates all end-token spelling differences so the slicer, + the body splitter, and unit visitors share one closing-rule table. + + Example: + Both ``end procedure step`` and ``end subroutine step`` close a + procedure slice, while only ``end module`` closes a module slice. + """ + lower = line.strip().lower() + if kind == "module": + return lower.startswith("end module") + if kind == "submodule": + return lower.startswith("end submodule") + if kind == "program": + return lower.startswith("end program") + if kind == "block_data": + return lower.startswith("end block data") or lower == "end" + if kind == "interface": + return lower.startswith("end interface") + if kind == "derived_type": + return lower.startswith("end type") + if kind == "procedure": + return ( + lower.startswith("end subroutine") + or lower.startswith("end function") + or lower.startswith("end procedure") + or lower == "end" + ) + return False + + def _helper_has_unit_end_ahead(self, lines: _PreprocessedLines, start_index: int, kind: str) -> bool: + """Check whether a candidate unit opener has a later matching end. + + The slicer uses this conservative look-ahead for ambiguous lines such + as ``type :: state``. With a later ``end type`` it is a derived-type + unit; without one the existing parser treats it as a declaration-like + line and continues. + + Example: + ``type :: particle`` followed by ``end type particle`` returns + `True`, but a lone ``type :: local_state`` in a program + specification part returns `False`. + """ + for idx in range(start_index + 1, len(lines)): + if self._helper_unit_end_matches(kind, lines[idx][0]): + return True + return False + + def _helper_find_unit_end(self, lines: _PreprocessedLines, start_index: int, kind: str) -> int | None: + """Find the matching end line for a source unit. + + The helper walks nested parseable units with a small stack. It is used + before building a `_SourceUnit`, so every visitor receives only its own + substring and original line numbers remain attached to each tuple. + + Example: + Given a module containing an interface containing a subroutine, the + module end is returned, not the subroutine end, because the nested + interface/procedure pair is pushed and popped before the module is + closed. + """ + stack = [kind] + idx = start_index + 1 + while idx < len(lines): + line = lines[idx][0].strip() + if not line: + idx += 1 + continue + start = self._helper_classify_unit_start(line) + if stack[-1] == "interface" and _MOD_PROC_IMPL_RE.match(line): + idx += 1 + continue + if start is not None and self._helper_has_unit_end_ahead(lines, idx, start[0]): + nested_kind, _ = start + stack.append(nested_kind) + idx += 1 + continue + if self._helper_unit_end_matches(stack[-1], line): + stack.pop() + if not stack: + return idx + idx += 1 + return None + + def _helper_slice_child_units( + self, + lines: _PreprocessedLines, + *, + parent_scope: _ParserScope, + allowed_kinds: set[str] | None = None, + ) -> list[_SourceUnit]: + """Slice direct child units from a parent source substring. + + The name says "child" because this helper is recursive by design: + `visit_file` calls it for file-level units, `visit_module_unit` calls it + for module children, and `visit_interface_unit` calls it for interface + procedure declarations. Each returned `_SourceUnit.lines` contains only + that child unit's substring. + + Example: + In ``module m`` with an ``interface`` block and a contained + ``subroutine run``, this helper returns two direct children for the + module scope: one interface unit and one procedure unit. The + interface's subroutine is not returned at module level; it is + returned when the interface visitor asks for its own children. + """ + units: list[_SourceUnit] = [] + index = 0 + while index < len(lines): + line, lineno, _ = lines[index] + if parent_scope.kind == "interface" and _MOD_PROC_IMPL_RE.match(line.strip()): + index += 1 + continue + start = self._helper_classify_unit_start(line) + if start is None: + index += 1 + continue + kind, name = start + if allowed_kinds is not None and kind not in allowed_kinds: + index += 1 + continue + + end_index = self._helper_find_unit_end(lines, index, kind) + if end_index is None: + if kind in {"derived_type", "interface"}: + # A `type :: name` statement without a matching `end type` + # is treated as a declaration-like line for compatibility + # with existing tolerant parser behavior. + index += 1 + continue + end_index = len(lines) - 1 + + end_line = lines[end_index][1] + units.append( + _SourceUnit( + kind=kind, + name=name, + lines=lines[index : end_index + 1], + start_line=lineno, + end_line=end_line, + ) + ) + index = end_index + 1 + return units + + def _helper_split_unit_parts(self, unit: _SourceUnit, grammar: _UnitGrammar) -> _UnitParts: + """Split one unit substring into grammar regions. + + The helper follows the shape you described: every parseable unit has a + header and a specification part, some have an execution part, and some + have a contains part. Visitors can then be small and choose which + region matters for wrapping metadata. + + Example: + For a procedure, declarations before the first executable + statement go into `specification`, assignments/calls go into + `execution`, and internal procedures after `contains` go into + `contains`. The procedure visitor parses only `specification`. + """ + header = unit.lines[0] if unit.lines else None + footer = unit.lines[-1] if unit.lines and self._helper_unit_end_matches(unit.kind, unit.lines[-1][0]) else None + body = unit.lines[1:-1] if footer is not None else unit.lines[1:] + specification: _PreprocessedLines = [] + execution: _PreprocessedLines = [] + contains: _PreprocessedLines = [] + region = "specification" + index = 0 + + while index < len(body): + line, _, _ = body[index] + stripped = line.strip() + if not stripped: + index += 1 + continue + if self._is_contains_transition(stripped): + region = "contains" + index += 1 + continue + + start = self._helper_classify_unit_start(stripped) + if start is not None: + child_kind, _ = start + child_end = self._helper_find_unit_end(body, index, child_kind) + if child_end is not None: + index = child_end + 1 + continue + + if ( + region == "specification" + and grammar.has_execution_part + and _is_executable_statement_start(stripped) + ): + region = "execution" + + if region == "specification": + specification.append(body[index]) + elif region == "execution": + execution.append(body[index]) + else: + contains.append(body[index]) + index += 1 + + return _UnitParts( + header=header, + specification=specification, + execution=execution, + contains=contains, + footer=footer, + ) + + def _helper_validate_unit_headers(self, lines: _PreprocessedLines, filename: str | None) -> None: + """Validate recognizable unit headers before slicing hides bad ones. + + The slicer intentionally ignores lines that are not valid starts. This + helper preserves diagnostics for malformed headers whose first keyword + still shows the user's intent. + + Example: + ``module :: bad_mod`` is not a valid module unit and therefore is + not returned by `_helper_slice_child_units`; this helper raises the + same explicit "malformed module header" error before parsing + continues. + """ + for line, lineno, source_line in lines: + stripped = line.strip() + if not stripped: + continue + self._parse_module_header(stripped, filename, lineno=lineno, source_line=source_line) + + def _helper_scope_for_model( + self, + kind: str, + model: object, + *, + parent: _ParserScope | None = None, + module_owner: str | None = None, + state: dict | None = None, + ) -> _ParserScope: + """Build the scope object passed through shared helpers. + + This helper keeps scope construction consistent: every unit visitor + passes its own model, parent scope, current module owner, and optional + procedure state through the same small object. + + Example: + A module procedure receives a procedure scope whose parent is the + module scope and whose `module_owner` is the module name. The same + declaration helper can then update procedure arguments while still + knowing the owner for diagnostics and imports. + """ + name = getattr(model, "name", None) + inherited_owner = module_owner if module_owner is not None else (parent.module_owner if parent else None) + if kind in {"module", "submodule"}: + inherited_owner = name + return _ParserScope( + kind=kind, + name=name, + model=model, + parent=parent, + module_owner=inherited_owner, + state=state, + ) + def _track_containing_module_scope(self, current_module: str | None, line: str) -> tuple[str | None, bool]: """Track module/submodule ownership for nested type/interface passes.""" lower = line.lower() @@ -1911,6 +2311,257 @@ def visit_fortran_block_data_unit(self, code: _SourceOrLines, filename: str | No filename=filename, ) + def visit_source_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + ): + """Visit one already-sliced source unit and dispatch by unit kind.""" + if unit.kind == "module": + return self.visit_module_unit(unit, parent_scope=parent_scope, filename=filename) + if unit.kind == "submodule": + return self.visit_submodule_unit(unit, parent_scope=parent_scope, filename=filename) + if unit.kind == "program": + return self.visit_program_unit(unit, parent_scope=parent_scope, filename=filename) + if unit.kind == "block_data": + return self.visit_block_data_source_unit(unit, parent_scope=parent_scope, filename=filename) + if unit.kind == "derived_type": + return self.visit_derived_type_unit(unit, parent_scope=parent_scope, filename=filename) + if unit.kind == "interface": + return self.visit_interface_unit(unit, parent_scope=parent_scope, filename=filename) + if unit.kind == "procedure": + return self.visit_procedure_unit(unit, parent_scope=parent_scope, filename=filename) + return None + + def visit_module_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + signatures: list[FortranProcedureSignature] | None = None, + types: list[FortranDerivedType] | None = None, + interfaces: list[FortranInterface] | None = None, + ) -> FortranModule: + """Visit a sliced `module ... end module` unit.""" + header = unit.lines[0] + module = self._parse_module_header(header[0].strip(), filename, lineno=header[1], source_line=header[2]) + if module is None: # pragma: no cover - slicer only dispatches module units with module headers. + raise FortranParseError("Expected module unit.", filename=filename, line_number=header[1], source_line=header[2]) + scope = self._helper_scope_for_model("module", module, parent=parent_scope) + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("module")) + self._helper_visit_spec_part(scope, parts.specification, filename=filename) + + child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope) + if signatures is None: + signatures = [ + self.visit_procedure_unit(child, parent_scope=scope, filename=filename) + for child in child_units + if child.kind == "procedure" + ] + if types is None: + types = [ + self.visit_derived_type_unit(child, parent_scope=scope, filename=filename) + for child in child_units + if child.kind == "derived_type" + ] + if interfaces is None: + interfaces = [ + self.visit_interface_unit(child, parent_scope=scope, filename=filename) + for child in child_units + if child.kind == "interface" + ] + self._attach_module_children( + [module], + signatures=signatures, + types=types, + interfaces=interfaces, + ) + _validate_module_variables(module, filename) + _apply_module_visibility(module, filename) + return module + + def visit_submodule_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + signatures: list[FortranProcedureSignature] | None = None, + types: list[FortranDerivedType] | None = None, + interfaces: list[FortranInterface] | None = None, + ) -> FortranSubmodule: + """Visit a sliced `submodule (...) name ... end submodule` unit.""" + header = unit.lines[0] + submodule = self._parse_submodule_header(header[0].strip(), filename) + if submodule is None: # pragma: no cover - slicer only dispatches submodule units with submodule headers. + raise FortranParseError("Expected submodule unit.", filename=filename, line_number=header[1], source_line=header[2]) + scope = self._helper_scope_for_model("submodule", submodule, parent=parent_scope) + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("submodule")) + self._helper_visit_spec_part(scope, parts.specification, filename=filename) + + child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope) + if signatures is None: + signatures = [ + self.visit_procedure_unit(child, parent_scope=scope, filename=filename) + for child in child_units + if child.kind == "procedure" + ] + if types is None: + types = [ + self.visit_derived_type_unit(child, parent_scope=scope, filename=filename) + for child in child_units + if child.kind == "derived_type" + ] + if interfaces is None: + interfaces = [ + self.visit_interface_unit(child, parent_scope=scope, filename=filename) + for child in child_units + if child.kind == "interface" + ] + self._attach_submodule_children( + [submodule], + signatures=signatures, + types=types, + interfaces=interfaces, + ) + _validate_module_variables(submodule, filename) + return submodule + + def visit_program_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + ) -> FortranProgram: + """Visit a sliced `program ... end program` unit.""" + header = unit.lines[0] + program = self._parse_program_header(header[0].strip(), filename) + if program is None: # pragma: no cover - slicer only dispatches program units with program headers. + raise FortranParseError("Expected program unit.", filename=filename, line_number=header[1], source_line=header[2]) + scope = self._helper_scope_for_model("program", program, parent=parent_scope) + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("program")) + self._helper_visit_spec_part(scope, parts.specification, filename=filename) + _validate_variable_declarations( + program.variables, + owner_kind="program", + owner_name=program.name, + filename=filename, + ) + return program + + def visit_block_data_source_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + ) -> FortranBlockData: + """Visit a sliced `block data ... end block data` unit.""" + header = unit.lines[0] + block_data = self._parse_block_data_header(header[0].strip(), filename) + if block_data is None: # pragma: no cover - slicer only dispatches block-data units with block-data headers. + raise FortranParseError("Expected block data unit.", filename=filename, line_number=header[1], source_line=header[2]) + scope = self._helper_scope_for_model("block_data", block_data, parent=parent_scope) + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("block_data")) + self._helper_visit_spec_part(scope, parts.specification, filename=filename) + _validate_variable_declarations( + block_data.variables, + owner_kind="block data", + owner_name=block_data.name, + filename=filename, + ) + return block_data + + def visit_derived_type_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + ) -> FortranDerivedType: + """Visit a sliced derived-type definition.""" + header = unit.lines[0] + dtype = self._init_derived_type(header[0].strip(), current_module=parent_scope.module_owner) + if dtype is None: # pragma: no cover - slicer only dispatches derived-type units with type headers. + raise FortranParseError("Expected derived-type unit.", filename=filename, line_number=header[1], source_line=header[2]) + scope = self._helper_scope_for_model("derived_type", dtype, parent=parent_scope) + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("derived_type")) + self._helper_visit_spec_part(scope, parts.specification, filename=filename) + for line, lineno, source_line in parts.contains: + stripped = line.strip() + if not stripped: + continue + self._parse_derived_type_contains_line( + stripped, + dtype, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + _validate_derived_type_fields(dtype, filename) + return dtype + + def visit_interface_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + ) -> FortranInterface: + """Visit a sliced interface block.""" + header = unit.lines[0] + starts_interface, interface_name = self._parse_interface_header(header[0].strip()) + if not starts_interface: # pragma: no cover - slicer only dispatches interface units with interface headers. + raise FortranParseError("Expected interface unit.", filename=filename, line_number=header[1], source_line=header[2]) + interface = FortranInterface(name=interface_name, module=parent_scope.module_owner) + scope = self._helper_scope_for_model("interface", interface, parent=parent_scope) + child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope, allowed_kinds={"procedure"}) + for child in child_units: + sig = self.visit_procedure_unit(child, parent_scope=scope, filename=filename, in_interface=True) + self._add_interface_attribute(sig, interface.name) + interface.procedures.append(sig) + return interface + + def visit_procedure_unit( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + in_interface: bool = False, + ) -> FortranProcedureSignature: + """Visit a sliced procedure body or interface procedure declaration.""" + header = unit.lines[0] + proc_state = self._parse_procedure_header( + header[0].strip(), + parent_scope.module_owner, + in_interface or parent_scope.kind == "interface", + filename=filename, + lineno=header[1], + source_line=header[2], + ) + if proc_state is None: + self._raise_if_unparsed_procedure_header( + header[0].strip(), + in_interface=in_interface or parent_scope.kind == "interface", + filename=filename, + lineno=header[1], + source_line=header[2], + ) + raise FortranParseError("Expected procedure unit.", filename=filename, line_number=header[1], source_line=header[2]) + proc_state["filename"] = filename + proc_state["header_lineno"] = header[1] + proc_state["header_source_line"] = header[2] + proc_state["uses"].update(getattr(parent_scope.model, "uses", {})) + scope = self._helper_scope_for_model("procedure", proc_state["signature"], parent=parent_scope, state=proc_state) + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("procedure")) + self._helper_visit_spec_part(scope, parts.specification, filename=filename) + return _finalize_proc(proc_state) + def _parse_fortran_types(self, code: _SourceOrLines, filename: str | None = None) -> list[FortranDerivedType]: # pragma: no cover - private compatibility shim. return self.visit_fortran_types(code, filename=filename) @@ -2294,6 +2945,219 @@ def _raise_if_unparsed_procedure_header( source_line=source_line, ) + def _helper_apply_meta_to_variable(self, var: FortranVariable, meta: dict, shape: list[str]) -> None: + """Apply parsed declaration metadata to a variable-like model. + + This helper exists because all declaration targets share the same core + type/kind/rank/shape fields, even when their containers differ. + + Example: + ``real(kind=rk), dimension(n) :: x`` produces the same base + metadata whether `x` is a module variable, a program variable, or a + derived-type field. The caller decides where the variable is stored. + """ + var.base_type = meta["base_type"] + var.kind = meta["kind"] + if shape: + var.shape = shape + var.rank = len(shape) + else: + var.shape = list(meta["shape"]) + var.rank = meta["rank"] + var.lbound, var.ubound = _extract_bounds(var.shape) + + def _helper_push_declaration_to_scope( + self, + scope: _ParserScope, + *, + meta: dict, + right: str, + role: str, + filename: str | None, + lineno: int | None, + source_line: str | None, + ) -> None: + """Push parsed declaration entities into the correct scope model. + + The name says "push" because parsing a declaration is only half of the + job; the other half is storing the resulting symbol in the active unit + scope. This helper is the common storage point for variables, fields, + and procedure arguments/results. + + Example: + ``integer :: n`` in a module appends `FortranVariable("n")` to + `module.variables`, while the same declaration inside + ``subroutine step(n)`` updates the existing `FortranArgument("n")` + in the procedure signature. + """ + if role == "procedure_symbol": + proc_state = scope.state + if proc_state is None: # pragma: no cover - internal helper misuse. + raise FortranParseError("Procedure declaration scope is missing state.", filename=filename) + if meta["base_type"] == "procedure" and meta["kind"] in proc_state.get("imports", set()): + meta["kind"] = None + for entity in split_csv(right): + raw_name, shape = _var(entity) + if not raw_name: + continue + normalized_name = _normalize_declared_name(raw_name, meta) + if not normalized_name: + continue + lowered_name = self._proc_scope_mark_declared_symbol( + proc_state, + normalized_name, + filename=filename, + line_number=lineno, + source_line=source_line, + ) + if meta.get("external"): + self._proc_scope_add_external_symbol(proc_state, lowered_name) + arg = self._proc_scope_get_symbol(proc_state, lowered_name) + if arg is None: + self._proc_scope_set_declared_local_type(proc_state, lowered_name, meta) + continue + _apply(arg, meta, shape) + return + + target = scope.model + if target is None: # pragma: no cover - internal helper misuse. + raise FortranParseError("Declaration scope is missing a target model.", filename=filename) + + for entity in split_csv(right): + raw_name, shape = _var(entity) + if not raw_name: + continue + normalized_name = _normalize_declared_name(raw_name, meta) + if not normalized_name: + continue + if role == "type_field": + field = FortranArgument(name=normalized_name) + _apply(field, meta, shape) + target.fields.append(field) + continue + var = FortranArgument(name=normalized_name) + _apply(var, meta, shape) + target.variables.append(var) + + def _helper_parse_declaration_line( + self, + line: str, + scope: _ParserScope, + *, + role: str, + filename: str | None, + lineno: int | None, + source_line: str | None, + include_intent: bool = False, + parse_character_star: bool = True, + ) -> bool: + """Parse one declaration line and store it in `scope`. + + This is the common declaration backend for module variables, program + variables, block-data variables, derived-type fields, and procedure + arguments/results. The `role` argument captures the small differences + in where the parsed symbol is pushed. + + Example: + ``real, intent(in) :: x(:)`` with role ``procedure_symbol`` updates + the active procedure argument. ``real :: x(:)`` with role + ``module_variable`` appends a `FortranVariable` to the module. + """ + parsed_decl = _parse_common_declaration_line( + line, + filename=filename, + line_number=lineno, + source_line=source_line, + include_intent=include_intent, + parse_character_star=parse_character_star, + ) + if parsed_decl is None: + return False + meta, right = parsed_decl + self._helper_push_declaration_to_scope( + scope, + meta=meta, + right=right, + role=role, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + return True + + def _helper_visit_spec_part( + self, + scope: _ParserScope, + lines: _PreprocessedLines, + *, + filename: str | None, + ) -> None: + """Visit declaration/specification lines for a unit scope. + + The helper name mirrors the grammar term "specification part". It is + called by module, submodule, program, procedure, derived-type, and + block-data visitors after `_helper_split_unit_parts` has isolated the + relevant region. + + Example: + A program and a procedure both have executable statements, but this + helper only sees their declaration lines before execution begins. + The visitor decides whether later execution/contains regions are + ignored or visited separately. + """ + for line, lineno, source_line in lines: + stripped = line.strip() + if not stripped: + continue + if scope.kind == "procedure": + self._parse_procedure_declaration_line( + stripped, + scope.state, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + elif scope.kind == "derived_type": + self._parse_type_field_line( + stripped, + scope.model, + filename, + lineno=lineno, + source_line=source_line, + ) + elif scope.kind == "module": + self._collect_module_spec_line( + scope.model, + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + elif scope.kind == "submodule": + self._collect_submodule_spec_line( + scope.model, + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + elif scope.kind == "program": + self._collect_program_spec_line( + scope.model, + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + elif scope.kind == "block_data": + self._collect_block_data_line( + scope.model, + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + # ------------------------------------------------------------------ # Procedure declaration parsing helpers # ------------------------------------------------------------------ @@ -2436,35 +3300,21 @@ def _apply_declaration_to_procedure_state( lineno: int | None, source_line: str | None, ) -> None: - if meta["base_type"] == "procedure" and meta["kind"] in proc_state.get("imports", set()): - meta["kind"] = None - - for entity in split_csv(right): - raw_name, shape = _var(entity) - if not raw_name: - continue - normalized_name = _normalize_declared_name(raw_name, meta) - if not normalized_name: - continue - lowered_name = self._proc_scope_mark_declared_symbol( - proc_state, - normalized_name, - filename=filename, - line_number=lineno, - source_line=source_line, - ) - if meta.get("external"): - self._proc_scope_add_external_symbol(proc_state, lowered_name) - # Legacy star-kind declarations can appear as: - # COMPLEX*16 AP(*), X(*) - # In this case the first parsed entity may carry the `*16` token - # in `raw_name`; always resolve symbols using the normalized name so - # all listed variables receive the declaration metadata. - arg = self._proc_scope_get_symbol(proc_state, lowered_name) - if arg is None: - self._proc_scope_set_declared_local_type(proc_state, lowered_name, meta) - continue - _apply(arg, meta, shape) + self._helper_push_declaration_to_scope( + _ParserScope( + kind="procedure", + name=proc_state["signature"].name, + model=proc_state["signature"], + state=proc_state, + module_owner=proc_state["signature"].module, + ), + meta=meta, + right=right, + role="procedure_symbol", + filename=filename, + lineno=lineno, + source_line=source_line, + ) def _collect_procedure_signatures( self, @@ -2652,14 +3502,22 @@ def _parse_procedure_declaration_line(self, line: str, proc_state: dict, filenam ): return - parsed_decl = _parse_common_declaration_line( + parsed = self._helper_parse_declaration_line( stripped, + _ParserScope( + kind="procedure", + name=proc_state["signature"].name, + model=proc_state["signature"], + state=proc_state, + module_owner=proc_state["signature"].module, + ), + role="procedure_symbol", filename=proc_state.get("filename") or filename, - line_number=lineno, + lineno=lineno, source_line=source_line, include_intent=True, ) - if parsed_decl is None: + if not parsed: self._handle_unknown_proc_declaration( line, proc_state, @@ -2669,16 +3527,6 @@ def _parse_procedure_declaration_line(self, line: str, proc_state: dict, filenam ) return - meta, right = parsed_decl - self._apply_declaration_to_procedure_state( - meta, - right, - proc_state, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - # ------------------------------------------------------------------ # Module/submodule/program/block-data variable declarations @@ -2754,14 +3602,16 @@ def _parse_module_variable_line(self, line: str, module: FortranModule | Fortran source_line=source_line, ) meta, right = parsed_decl - - for v in split_csv(right): - name, shape = _var(v) - if not name: - continue - var = FortranArgument(name=_normalize_declared_name(name, meta)) - _apply(var, meta, shape) - module.variables.append(var) + owner_kind, owner_name = _variable_scope_label(module) + self._helper_push_declaration_to_scope( + _ParserScope(kind=owner_kind, name=owner_name, model=module), + meta=meta, + right=right, + role="module_variable", + filename=filename, + lineno=lineno, + source_line=source_line, + ) def _parse_single_procedure_signature( self, code: _SourceOrLines, @@ -2852,26 +3702,46 @@ def visit_fortran_file( effective_macro_defines = self.macro_defines if macro_defines is None else macro_defines lines = preprocess_lines(code, filename) + self._helper_validate_unit_headers(lines, filename) + root_scope = _ParserScope(kind="file", name=None) + top_units = self._helper_slice_child_units(lines, parent_scope=root_scope) signatures = self._collect_procedure_signatures(lines, filename=filename, macro_defines=effective_macro_defines) derived_types = self.visit_fortran_types(lines, filename=filename) interfaces = self.visit_fortran_interfaces(lines, filename=filename) - modules = self.visit_fortran_modules( - lines, - filename=filename, - require_present=False, - signatures=signatures, - types=derived_types, - interfaces=interfaces, - ) - submodules = self.visit_fortran_submodules( - lines, - filename=filename, - signatures=signatures, - types=derived_types, - interfaces=interfaces, - ) - programs = self.visit_fortran_programs(lines, filename=filename) - block_data_units = self.visit_fortran_block_data(lines, filename=filename) + modules = [ + self.visit_module_unit( + unit, + parent_scope=root_scope, + filename=filename, + signatures=signatures, + types=derived_types, + interfaces=interfaces, + ) + for unit in top_units + if unit.kind == "module" + ] + submodules = [ + self.visit_submodule_unit( + unit, + parent_scope=root_scope, + filename=filename, + signatures=signatures, + types=derived_types, + interfaces=interfaces, + ) + for unit in top_units + if unit.kind == "submodule" + ] + programs = [ + self.visit_program_unit(unit, parent_scope=root_scope, filename=filename) + for unit in top_units + if unit.kind == "program" + ] + block_data_units = [ + self.visit_block_data_source_unit(unit, parent_scope=root_scope, filename=filename) + for unit in top_units + if unit.kind == "block_data" + ] owned_proc_ids = {id(proc) for mod in modules for proc in mod.procedures} owned_proc_ids.update(id(proc) for submod in submodules for proc in submod.procedures) @@ -3073,49 +3943,37 @@ def _parse_fortran_types_impl(self, code: _SourceOrLines, filename: str | None = bindings and generic bindings until `end type`. """ lines = self._preprocessed_lines(code, filename) - current_module = None - current_type: FortranDerivedType | None = None - in_type_contains = False types: list[FortranDerivedType] = [] + root_scope = _ParserScope(kind="file", name=None) for line, lineno, source_line in lines: - s = line.strip() - if not s: - continue - l = s.lower() - _enforce_source_form_compatibility(s, filename, lineno, source_line) - - current_module, handled_scope = self._track_containing_module_scope(current_module, s) - if handled_scope: - continue + if line.strip(): + _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) - if current_type is None: - current_type = self._init_derived_type(s, current_module=current_module) - if current_type is not None: - in_type_contains = False - continue - - if self._is_contains_transition(s): - in_type_contains = True - continue - - if l.startswith("end type"): - self._finalize_derived_type(current_type, types, filename) - current_type = None - in_type_contains = False - continue - - if in_type_contains: - self._parse_derived_type_contains_line( - s, - current_type, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - continue + def collect(scope: _ParserScope, source_lines: _PreprocessedLines) -> None: + for child in self._helper_slice_child_units(source_lines, parent_scope=scope): + if child.kind == "derived_type": + types.append(self.visit_derived_type_unit(child, parent_scope=scope, filename=filename)) + continue + if child.kind in {"module", "submodule", "program"}: + child_scope = _ParserScope( + kind=child.kind, + name=child.name, + parent=scope, + module_owner=child.name if child.kind in {"module", "submodule"} else scope.module_owner, + ) + collect(child_scope, child.lines[1:-1]) + continue + if child.kind in {"procedure", "block_data"}: + child_scope = _ParserScope( + kind=child.kind, + name=child.name, + parent=scope, + module_owner=scope.module_owner, + ) + collect(child_scope, child.lines[1:-1]) - self._parse_type_field_line(s, current_type, filename, lineno=lineno, source_line=source_line) + collect(root_scope, lines) self._resolve_derived_type_extensions(types) return types @@ -3151,14 +4009,15 @@ def _parse_type_field_line(self, line: str, dtype: FortranDerivedType, filename: source_line=source_line, ) meta, right = parsed_decl - - for v in split_csv(right): - name, shape = _var(v) - if not name: - continue - field = FortranArgument(name=_normalize_declared_name(name, meta)) - _apply(field, meta, shape) - dtype.fields.append(field) + self._helper_push_declaration_to_scope( + _ParserScope(kind="derived_type", name=dtype.name, model=dtype, module_owner=dtype.module), + meta=meta, + right=right, + role="type_field", + filename=filename, + lineno=lineno, + source_line=source_line, + ) def _parse_derived_type_start(self, line: str) -> tuple[str, list[str]] | None: stripped = line.strip() @@ -3200,77 +4059,25 @@ def _parse_fortran_modules_impl( names, preserving ownership and visibility rules. """ lines = self._preprocessed_lines(code, filename) - modules: list[FortranModule] = [] - current: FortranModule | None = None - in_contains = False - interface_depth = 0 - type_depth = 0 - + self._helper_validate_unit_headers(lines, filename) for line, lineno, source_line in lines: - s = line.strip() - if not s: - continue - l = s.lower() - _enforce_source_form_compatibility(s, filename, lineno, source_line) - - module = self._parse_module_header(s, filename, lineno=lineno, source_line=source_line) - if module is not None: - current = module - in_contains = False - interface_depth = 0 - type_depth = 0 - continue - - if l.startswith("end module"): - if current is not None: - self._finalize_module(current, modules, filename) - current = None - in_contains = False - interface_depth = 0 - type_depth = 0 - continue - - if current is None: - continue - - if self._is_contains_transition(s): - in_contains = True - continue - if in_contains: - continue - - if _is_derived_type_block_start(s): - type_depth += 1 - continue - if l.startswith("end type"): - type_depth = max(0, type_depth - 1) - continue - if l.startswith("interface") or l.startswith("abstract interface"): - interface_depth += 1 - continue - if l.startswith("end interface"): - interface_depth = max(0, interface_depth - 1) - continue - if interface_depth > 0 or type_depth > 0: - continue - - self._collect_module_spec_line( - current, - s, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - + if line.strip(): + _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) signatures = self._collect_procedure_signatures(code, filename) if signatures is None else signatures types = self._parse_fortran_types(code, filename) if types is None else types interfaces = self._parse_fortran_interfaces(code, filename) if interfaces is None else interfaces - self._attach_module_children( - modules, - signatures=signatures, - types=types, - interfaces=interfaces, - ) + root_scope = _ParserScope(kind="file", name=None) + modules = [ + self.visit_module_unit( + unit, + parent_scope=root_scope, + filename=filename, + signatures=signatures, + types=types, + interfaces=interfaces, + ) + for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"module"}) + ] if require_present and not modules and signatures: raise FortranParseError( "_parse_fortran_modules() expected a module program unit, but only standalone procedures were found", @@ -3328,69 +4135,37 @@ def _parse_fortran_interfaces_impl(self, code: _SourceOrLines, filename: str | N annotates them as interface members. """ lines = self._preprocessed_lines(code, filename) - current_module = None - current_interface: FortranInterface | None = None - current_proc = None interfaces: list[FortranInterface] = [] + root_scope = _ParserScope(kind="file", name=None) for line, lineno, source_line in lines: - s = line.strip() - if not s: - continue - l = s.lower() - _enforce_source_form_compatibility(s, filename, lineno, source_line) - - current_module, handled_scope = self._track_containing_module_scope(current_module, s) - if handled_scope: - continue - - starts_interface, interface_name = self._parse_interface_header(s) - if starts_interface: - current_interface = FortranInterface(name=interface_name, module=current_module) - current_proc = None - continue - - if l.startswith("end interface"): - if current_proc is not None and current_interface is not None: - self._finalize_interface_procedure(current_proc, current_interface) - current_proc = None - if current_interface is not None: - self._finalize_interface(current_interface, interfaces) - current_interface = None - continue + if line.strip(): + _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) - if current_interface is None: - continue - - if current_proc is None: - parsed = self._parse_interface_procedure_header( - s, - current_module, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - if parsed: - current_proc = parsed - current_proc["filename"] = filename - current_proc["header_lineno"] = lineno - current_proc["header_source_line"] = source_line - else: - self._raise_if_unparsed_procedure_header( - s, - in_interface=True, - filename=filename, - lineno=lineno, - source_line=source_line, + def collect(scope: _ParserScope, source_lines: _PreprocessedLines) -> None: + for child in self._helper_slice_child_units(source_lines, parent_scope=scope): + if child.kind == "interface": + interfaces.append(self.visit_interface_unit(child, parent_scope=scope, filename=filename)) + continue + if child.kind in {"module", "submodule"}: + child_scope = _ParserScope( + kind=child.kind, + name=child.name, + parent=scope, + module_owner=child.name, ) - continue - - if l.startswith("end subroutine") or l.startswith("end function") or l == "end": - self._finalize_interface_procedure(current_proc, current_interface) - current_proc = None - continue + collect(child_scope, child.lines[1:-1]) + continue + if child.kind in {"procedure", "program", "block_data"}: + child_scope = _ParserScope( + kind=child.kind, + name=child.name, + parent=scope, + module_owner=scope.module_owner, + ) + collect(child_scope, child.lines[1:-1]) - self._parse_procedure_declaration_line(s, current_proc, filename=filename, lineno=lineno, source_line=source_line) + collect(root_scope, lines) return interfaces @@ -3512,77 +4287,25 @@ def _parse_fortran_submodules_impl( submodule name. """ lines = self._preprocessed_lines(code, filename) - submodules: list[FortranSubmodule] = [] - current: FortranSubmodule | None = None - in_contains = False - interface_depth = 0 - type_depth = 0 - + self._helper_validate_unit_headers(lines, filename) for line, lineno, source_line in lines: - s = line.strip() - if not s: - continue - l = s.lower() - _enforce_source_form_compatibility(s, filename, lineno, source_line) - - submodule = self._parse_submodule_header(s, filename) - if submodule is not None: - current = submodule - in_contains = False - interface_depth = 0 - type_depth = 0 - continue - - if l.startswith("end submodule"): - if current is not None: - self._finalize_submodule(current, submodules, filename) - current = None - in_contains = False - interface_depth = 0 - type_depth = 0 - continue - - if current is None: - continue - - if self._is_contains_transition(s): - in_contains = True - continue - if in_contains: - continue - - if _is_derived_type_block_start(s): - type_depth += 1 - continue - if l.startswith("end type"): - type_depth = max(0, type_depth - 1) - continue - if l.startswith("interface") or l.startswith("abstract interface"): - interface_depth += 1 - continue - if l.startswith("end interface"): - interface_depth = max(0, interface_depth - 1) - continue - if interface_depth > 0 or type_depth > 0: - continue - - self._collect_submodule_spec_line( - current, - s, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - + if line.strip(): + _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) signatures = self._collect_procedure_signatures(code, filename) if signatures is None else signatures types = self._parse_fortran_types(code, filename) if types is None else types interfaces = self._parse_fortran_interfaces(code, filename) if interfaces is None else interfaces - self._attach_submodule_children( - submodules, - signatures=signatures, - types=types, - interfaces=interfaces, - ) + root_scope = _ParserScope(kind="file", name=None) + submodules = [ + self.visit_submodule_unit( + unit, + parent_scope=root_scope, + filename=filename, + signatures=signatures, + types=types, + interfaces=interfaces, + ) + for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"submodule"}) + ] return submodules def _parse_fortran_submodule(self, code: _SourceOrLines, filename: str | None = None) -> FortranSubmodule: # pragma: no cover - private compatibility shim. @@ -3602,56 +4325,15 @@ def _parse_fortran_programs_impl(self, code: _SourceOrLines, filename: str | Non - Ignores internal procedures in `contains` for program-variable scope. """ lines = self._preprocessed_lines(code, filename) - programs: list[FortranProgram] = [] - current: FortranProgram | None = None - in_contains = False - in_exec_part = False - + self._helper_validate_unit_headers(lines, filename) for line, lineno, source_line in lines: - s = line.strip() - if not s: - continue - l = s.lower() - _enforce_source_form_compatibility(s, filename, lineno, source_line) - - program = self._parse_program_header(s, filename) - if program is not None: - current = program - in_contains = False - in_exec_part = False - continue - - if l.startswith("end program"): - if current is not None: - self._finalize_program(current, programs, filename) - current = None - in_contains = False - in_exec_part = False - continue - - if current is None: - continue - - if self._is_contains_transition(s): - in_contains = True - in_exec_part = False - continue - if in_contains: - continue - if in_exec_part: - continue - if _is_executable_statement_start(s): - in_exec_part = True - continue - - self._collect_program_spec_line( - current, - s, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - return programs + if line.strip(): + _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) + root_scope = _ParserScope(kind="file", name=None) + return [ + self.visit_program_unit(unit, parent_scope=root_scope, filename=filename) + for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"program"}) + ] def _parse_fortran_program(self, code: _SourceOrLines, filename: str | None = None) -> FortranProgram: # pragma: no cover - private compatibility shim. return _expect_single_parse_result( @@ -3664,35 +4346,15 @@ def _parse_fortran_program(self, code: _SourceOrLines, filename: str | None = No def _parse_fortran_block_data(self, code: _SourceOrLines, filename: str | None = None) -> list[FortranBlockData]: """Parse `block data` units and their declaration sections.""" lines = self._preprocessed_lines(code, filename) - blocks: list[FortranBlockData] = [] - current: FortranBlockData | None = None - + self._helper_validate_unit_headers(lines, filename) for line, lineno, source_line in lines: - s = line.strip() - if not s: - continue - l = s.lower() - _enforce_source_form_compatibility(s, filename, lineno, source_line) - - block_data = self._parse_block_data_header(s, filename) - if block_data is not None: - current = block_data - continue - - if current is not None and (l.startswith("end block data") or l == "end"): - self._finalize_block_data(current, blocks, filename) - current = None - continue - if current is None: - continue - self._collect_block_data_line( - current, - s, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - return blocks + if line.strip(): + _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) + root_scope = _ParserScope(kind="file", name=None) + return [ + self.visit_block_data_source_unit(unit, parent_scope=root_scope, filename=filename) + for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"block_data"}) + ] def _parse_fortran_block_data_unit(self, code: _SourceOrLines, filename: str | None = None) -> FortranBlockData: # pragma: no cover - private compatibility shim. return _expect_single_parse_result( diff --git a/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSC.json b/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSC.json index 3579ed1b2..e9f089fc4 100644 --- a/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSC.json +++ b/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSC.json @@ -4578,7 +4578,22 @@ } ], "default_visibility": "private", - "public_symbols": [], + "public_symbols": [ + "sparse_dmatrix_csc", + "sparse_zmatrix_csc", + "as_sparse", + "sparse", + "assignment(=)", + "operator(+)", + "operator(-)", + "operator(*)", + "operator(/)", + "operator(.x.)", + "sp_kron", + "transpose", + "hconjg", + "matmul" + ], "private_symbols": [] } ], @@ -9166,7 +9181,22 @@ } ], "default_visibility": "private", - "public_symbols": [], + "public_symbols": [ + "sparse_dmatrix_csc", + "sparse_zmatrix_csc", + "as_sparse", + "sparse", + "assignment(=)", + "operator(+)", + "operator(-)", + "operator(*)", + "operator(/)", + "operator(.x.)", + "sp_kron", + "transpose", + "hconjg", + "matmul" + ], "private_symbols": [] } } diff --git a/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSR.json b/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSR.json index 4b441603e..e6e9206f4 100644 --- a/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSR.json +++ b/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_ARRAY_CSR.json @@ -4578,7 +4578,22 @@ } ], "default_visibility": "private", - "public_symbols": [], + "public_symbols": [ + "sparse_dmatrix_csr", + "sparse_zmatrix_csr", + "as_sparse", + "sparse", + "assignment(=)", + "operator(+)", + "operator(-)", + "operator(*)", + "operator(/)", + "operator(.x.)", + "kron", + "transpose", + "hconjg", + "matmul" + ], "private_symbols": [] } ], @@ -9166,7 +9181,22 @@ } ], "default_visibility": "private", - "public_symbols": [], + "public_symbols": [ + "sparse_dmatrix_csr", + "sparse_zmatrix_csr", + "as_sparse", + "sparse", + "assignment(=)", + "operator(+)", + "operator(-)", + "operator(*)", + "operator(/)", + "operator(.x.)", + "kron", + "transpose", + "hconjg", + "matmul" + ], "private_symbols": [] } } diff --git a/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_COMMON.json b/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_COMMON.json index a5c00202d..b7b2dd115 100644 --- a/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_COMMON.json +++ b/tests/parser/fortran/fixtures/scifortran/SF_SPARSE_COMMON.json @@ -577,7 +577,9 @@ } ], "default_visibility": "public", - "public_symbols": [], + "public_symbols": [ + "shape" + ], "private_symbols": [] } ], @@ -1164,7 +1166,9 @@ } ], "default_visibility": "public", - "public_symbols": [], + "public_symbols": [ + "shape" + ], "private_symbols": [] } } From 3f2b21803b45b65f03856d92b4a344222e4c8a6b Mon Sep 17 00:00:00 2001 From: said Date: Tue, 19 May 2026 11:33:59 +0100 Subject: [PATCH 2/2] update parser --- README.md | 8 + fortran_parser.md | 21 +- fortran_parser/parser.py | 1149 +++++------------ parser_implementation_reference.md | 41 +- .../fixtures/scifortran/fmin_cg_minimize.json | 792 ++---------- .../scifortran/integrate_quad_sample.json | 146 +-- 6 files changed, 506 insertions(+), 1651 deletions(-) diff --git a/README.md b/README.md index 56fb82b8e..d5d02c840 100644 --- a/README.md +++ b/README.md @@ -453,6 +453,14 @@ The parser exposes stable file/project entrypoints: - `parse_fortran_project(...)` for many sources returning `FortranProject`. - `assess_wrap_readiness(...)` for wrappability diagnostics. +Internally, `FortranParser.visit_file` uses a recursive source-unit parser: +the file is sliced into direct modules/submodules/programs/procedures/block +data/interfaces/types, then each unit visitor parses only its own substring and +recurses into direct children. Shared declaration helpers parse variables, +procedure arguments/results, and type fields, then push them into the active +scope. Procedure execution bodies and internal subprograms are ignored for +wrapper metadata; procedure-local interfaces are retained for callback typing. + The semantics layer consumes `FortranFile`/`FortranModule` objects and projects them into language-independent semantic IR (`SemanticModule`, `SemanticFunction`, `SemanticClass`, `SemanticType`). This keeps the semantic API model independent from parser internals, matching the project goal that parser output is a helper and the semantic interface/IR is the source of truth. For Fortran `use` imports, the parser stores each explicit imported symbol as a diff --git a/fortran_parser.md b/fortran_parser.md index e389c4a8e..0378a30f0 100644 --- a/fortran_parser.md +++ b/fortran_parser.md @@ -88,16 +88,25 @@ sections so maintainers can navigate the file by concern instead of by history: - `FortranParser` internals grouped by domain: - visitor-style API entrypoints (`visit_file`, `visit_project`, `visit_wrap_readiness`) - - signature/declaration parsing - - module-variable parsing - - file/project orchestration - - program-unit parsers (types, modules, interfaces, submodules, programs, - block-data) - - `_helper_*` methods for scoped parsing, expression resolution, and shared + - 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, + preprocessor branch selection, same-level duplicate checks, and shared specification-part collection - Thin module-level convenience wrappers that delegate to a shared parser instance +`visit_file` is the central orchestration path. It first slices the source into +direct file-level units, then each unit visitor parses only its own substring +and recursively slices direct children. Procedure execution parts are ignored +for wrapper metadata, and procedure-internal subprograms are not exported as +file/module procedures. Procedure-local interface blocks are still visited +enough to type callback dummy arguments and to preserve interface metadata. + Most parser organization changes are structural, but behavior, model-schema, coverage, or fixture changes should be reflected in this reference. diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index d0ba2cbe4..9b27014bb 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -78,8 +78,8 @@ ) _LEGACY_PARAM_STMT_RE = re.compile(r"^parameter\s*\(\s*(?P.*)\s*\)$", re.IGNORECASE) _DERIVED_TYPE_RE = re.compile(r"^type\s*(?P(?:,\s*[^:]+)?)::\s*(?P\w+)(?:\s*\([^)]*\))?$", re.IGNORECASE) -_TYPE_FIELD_RE = re.compile(r"^type\s*\(\s*(?P\w+)\s*\)\s*(?P.*)$", re.IGNORECASE) -_CLASS_FIELD_RE = re.compile(r"^class\s*\(\s*(?P\w+)\s*\)\s*(?P.*)$", re.IGNORECASE) +_TYPE_FIELD_RE = re.compile(r"^type\s*\(\s*(?P\w+(?:\s*\([^)]*\))?)\s*\)\s*(?P.*)$", re.IGNORECASE) +_CLASS_FIELD_RE = re.compile(r"^class\s*\(\s*(?P\w+(?:\s*\([^)]*\))?)\s*\)\s*(?P.*)$", re.IGNORECASE) _PROC_BIND_RE = re.compile(r"^procedure\s*(?:,\s*[^:]*)?::\s*(?P.*)$", re.IGNORECASE) _PROC_DUMMY_RE = re.compile(r"^procedure\s*\(\s*(?P\w+)\s*\)\s*(?P.*)$", re.IGNORECASE) _MODULE_RE = re.compile(r"^module\s+(?P\w+)\s*$", re.IGNORECASE) @@ -117,6 +117,7 @@ class _SourceUnit: lines: _PreprocessedLines start_line: int | None end_line: int | None + condition_set: frozenset[str] = frozenset() @dataclass @@ -285,6 +286,21 @@ def _parse_type_prefix(prefix: str) -> tuple[str, str | None] | None: return None # pragma: no cover - unsupported prefixes are rejected by public grammar. +def _enforce_source_form_compatibility( + line: str, + filename: str | None, + lineno: int | None = None, + source_line: str | None = None, +) -> None: + """Compatibility hook for source-form checks. + + The parser records fixed-form metadata, but it stays permissive about + modern constructs in `.f77` files because existing callers use the suffix + for layout, not strict language-version validation. + """ + return + + # ----------------------------------------------------------------------------- # Preprocessor and conditional-compilation helpers # ----------------------------------------------------------------------------- @@ -704,7 +720,8 @@ def _looks_like_procedure_header(line: str) -> bool: lowered = stripped.lower() if lowered.startswith(("end ", "call ")): return False - return bool(re.search(r"(?:^|[\s,])(?:subroutine|function)\s+[A-Za-z_]\w*", stripped, re.IGNORECASE)) + without_strings = re.sub(r"'[^']*'|\"[^\"]*\"", "", stripped) + return bool(re.search(r"(?:^|[\s,])(?:subroutine|function)\s+[A-Za-z_]\w*", without_strings, re.IGNORECASE)) def _is_openmp_directive(line: str) -> bool: @@ -1350,7 +1367,11 @@ def _resolve_signature_kinds( for name, var in sig.variables.items(): if var.value is not None: symbol_to_value.setdefault(name.lower(), var.value) - sig.variables.update(_resolve_variables(symbol_to_value)) + variable_base_types = {name.lower(): var.base_type for name, var in sig.variables.items()} + resolved_variables = _resolve_variables(symbol_to_value, variable_base_types) + for name in list(sig.variables): + if name.lower() in resolved_variables: + sig.variables[name] = resolved_variables[name.lower()] resolver = _CompileTimeResolver(symbol_to_value) for arg in sig.arguments: if arg.kind: @@ -1698,151 +1719,20 @@ def visit_file( macro_defines: set[str] | dict[str, int | bool | str] | None = None, encoding: str = "utf-8", ) -> FortranFile: - """Parse one source string/path into a `FortranFile` aggregate model.""" - if filename is None and _looks_like_existing_source_path(source_or_path): - path = Path(source_or_path) - filename = str(path) - code = path.read_text(encoding=encoding) - else: - code = str(source_or_path) - - effective_macro_defines = self.macro_defines if macro_defines is None else macro_defines - lines = preprocess_lines(code, filename) - signatures = self._collect_procedure_signatures(lines, filename=filename, macro_defines=effective_macro_defines) - derived_types = self.visit_fortran_types(lines, filename=filename) - interfaces = self.visit_fortran_interfaces(lines, filename=filename) - modules = self.visit_fortran_modules( - lines, - filename=filename, - require_present=False, - signatures=signatures, - types=derived_types, - interfaces=interfaces, - ) - submodules = self.visit_fortran_submodules( - lines, - filename=filename, - signatures=signatures, - types=derived_types, - interfaces=interfaces, - ) - programs = self.visit_fortran_programs(lines, filename=filename) - block_data_units = self.visit_fortran_block_data(lines, filename=filename) - variable_units = [*modules, *submodules, *programs, *block_data_units] - if any(var.kind or var.value is not None for unit in variable_units for var in getattr(unit, "variables", [])): - module_params = self._collect_module_parameters(lines, filename) - for unit in variable_units: - _resolve_module_variable_kinds(unit, module_params) - - owned_proc_ids = {id(proc) for mod in modules for proc in mod.procedures} - owned_proc_ids.update(id(proc) for submod in submodules for proc in submod.procedures) - standalone_procedures = [ - sig for sig in signatures - if sig.module is None and not sig.in_interface and id(sig) not in owned_proc_ids - ] - - file = FortranFile( + return self.visit_fortran_file( + source_or_path, filename=filename, - source=code, + macro_defines=macro_defines, encoding=encoding, - format=_source_form(filename), - modules=modules, - submodules=submodules, - programs=programs, - block_data_units=block_data_units, - procedures=standalone_procedures, - interfaces=[iface for iface in interfaces if iface.module is None], - derived_types=[dtype for dtype in derived_types if dtype.module is None], ) - for m in modules: - self._insert_unique_scope_symbol(file.symbols, m.name.lower(), m, label="file scope", filename=filename) - for sm in submodules: - self._insert_unique_scope_symbol(file.symbols, sm.name.lower(), sm, label="file scope", filename=filename) - for p in standalone_procedures: - self._insert_unique_scope_symbol(file.symbols, p.name.lower(), p, label="file scope", filename=filename) - return file - def visit_project( self, files: dict[str, str] | list[str | Path] | tuple[str | Path, ...] | str | Path, *, encoding: str = "utf-8", ) -> FortranProject: - """Parse many sources and merge them into one dependency-aware project model.""" - if isinstance(files, dict): - parsed_files = [self.visit_file(code, filename=fname, encoding=encoding) for fname, code in files.items()] - elif isinstance(files, (str, Path)): - namespace = self._helper_collect_namespace(files) - parsed_files = [self.visit_file(path, encoding=encoding) for path in namespace["files"]] - else: - parsed_files = [self.visit_file(path, encoding=encoding) for path in files] - - module_params: dict[str, dict[str, str]] = {} - for parsed_file in parsed_files: - if parsed_file.source is not None: - module_params.update(self._collect_module_parameters(parsed_file.source, parsed_file.filename)) - - seen_procedures: set[int] = set() - for parsed_file in parsed_files: - for proc in parsed_file.procedures: - if id(proc) not in seen_procedures: - _resolve_signature_kinds(proc, module_params, resolve_shapes=False) - seen_procedures.add(id(proc)) - for module in parsed_file.modules: - for proc in module.procedures: - if id(proc) not in seen_procedures: - _resolve_signature_kinds(proc, module_params, resolve_shapes=False) - seen_procedures.add(id(proc)) - for submodule in parsed_file.submodules: - for proc in submodule.procedures: - if id(proc) not in seen_procedures: - _resolve_signature_kinds(proc, module_params, resolve_shapes=False) - seen_procedures.add(id(proc)) - - project = FortranProject(files=parsed_files) - - for f in parsed_files: - for module in f.modules: - module_key = module.name.lower() - self._insert_unique_scope_symbol(project.modules, module_key, module, label="project module scope") - project.dependencies[module_key] = {name.lower() for name in module.uses} - for proc in module.procedures: - proc_key = f"{module_key}.{proc.name.lower()}" - self._insert_unique_scope_symbol(project.procedures, proc_key, proc, label="project procedure scope") - project.procedures.setdefault(proc.name.lower(), proc) - for dtype in module.derived_types: - dtype_key = f"{module_key}.{dtype.name.lower()}" - self._insert_unique_scope_symbol(project.derived_types, dtype_key, dtype, label="project derived-type scope") - project.derived_types.setdefault(dtype.name.lower(), dtype) - for iface in module.interfaces: - if iface.name: - iface_key = f"{module_key}.{iface.name.lower()}" - self._insert_unique_scope_symbol(project.interfaces, iface_key, iface, label="project interface scope") - project.interfaces.setdefault(iface.name.lower(), iface) - for submodule in f.submodules: - submodule_key = submodule.name.lower() - self._insert_unique_scope_symbol(project.submodules, submodule_key, submodule, label="project submodule scope") - deps = {submodule.parent.lower(), *(name.lower() for name in submodule.uses)} - if submodule.ancestor: - deps.add(submodule.ancestor.lower()) - project.dependencies[submodule_key] = deps - for proc in submodule.procedures: - proc_key = f"{submodule_key}.{proc.name.lower()}" - self._insert_unique_scope_symbol(project.procedures, proc_key, proc, label="project procedure scope") - project.procedures.setdefault(proc.name.lower(), proc) - for program in f.programs: - if program.name: - self._insert_unique_scope_symbol(project.programs, program.name.lower(), program, label="project program scope") - project.dependencies[program.name.lower()] = {name.lower() for name in program.uses} - for proc in f.procedures: - self._insert_unique_scope_symbol(project.procedures, proc.name.lower(), proc, label="project procedure scope") - for dtype in f.derived_types: - self._insert_unique_scope_symbol(project.derived_types, dtype.name.lower(), dtype, label="project derived-type scope") - for iface in f.interfaces: - if iface.name: - self._insert_unique_scope_symbol(project.interfaces, iface.name.lower(), iface, label="project interface scope") - return project + return self.visit_fortran_project(files, encoding=encoding) def visit_wrap_readiness(self, code: str, filename: str | None = None) -> dict: return self.visit_fortran_wrap_readiness(code, filename=filename) @@ -2070,6 +1960,8 @@ def _helper_classify_unit_start(self, line: str) -> tuple[str, str | None] | Non block_data = _BLOCK_DATA_RE.match(stripped) if block_data: return "block_data", block_data.group("name") + if lower == "enum" or lower.startswith("enum,"): + return "enum", None starts_interface, interface_name = self._parse_interface_header(stripped) if starts_interface: return "interface", interface_name @@ -2108,6 +2000,8 @@ def _helper_unit_end_matches(kind: str, line: str) -> bool: return lower.startswith("end interface") if kind == "derived_type": return lower.startswith("end type") + if kind == "enum": + return lower.startswith("end enum") if kind == "procedure": return ( lower.startswith("end subroutine") @@ -2195,8 +2089,23 @@ def _helper_slice_child_units( """ units: list[_SourceUnit] = [] index = 0 + pp_condition_stack: list[tuple[int, int]] = [] + pp_active_stack: list[bool] = [] + pp_group_counter = 0 while index < len(lines): line, lineno, _ = lines[index] + stripped = line.strip() + handled_pp, pp_group_counter = self._handle_procedure_preprocessor_line( + stripped, + macro_selection_enabled=False, + macro_names=set(), + pp_condition_stack=pp_condition_stack, + pp_active_stack=pp_active_stack, + pp_group_counter=pp_group_counter, + ) + if handled_pp: + index += 1 + continue if parent_scope.kind == "interface" and _MOD_PROC_IMPL_RE.match(line.strip()): index += 1 continue @@ -2227,11 +2136,82 @@ def _helper_slice_child_units( lines=lines[index : end_index + 1], start_line=lineno, end_line=end_line, + condition_set=self._procedure_preprocessor_condition_set(pp_condition_stack), ) ) index = end_index + 1 return units + def _helper_select_active_preprocessor_lines( + self, + lines: _PreprocessedLines, + macro_defines: set[str] | dict[str, int | bool | str] | None, + ) -> _PreprocessedLines: + """Drop inactive preprocessor branches when macro selection is enabled.""" + if macro_defines is None: + return lines + selected: _PreprocessedLines = [] + macro_names = _normalize_macro_defines(macro_defines) + pp_condition_stack: list[tuple[int, int]] = [] + pp_active_stack: list[bool] = [] + pp_group_counter = 0 + for line, _lineno, _source_line in lines: + handled_pp, pp_group_counter = self._handle_procedure_preprocessor_line( + line.strip(), + macro_selection_enabled=True, + macro_names=macro_names, + pp_condition_stack=pp_condition_stack, + pp_active_stack=pp_active_stack, + pp_group_counter=pp_group_counter, + ) + if handled_pp: + continue + if pp_active_stack and not all(pp_active_stack): + continue + selected.append((line, _lineno, _source_line)) + return selected + + def _helper_validate_sibling_units( + self, + units: list[_SourceUnit], + *, + parent_scope: _ParserScope, + filename: str | None, + ) -> None: + """Validate duplicate source-unit names at one scope level.""" + seen: dict[tuple[str, str], list[_SourceUnit]] = {} + for unit in units: + if not unit.name: + continue + if unit.kind == "procedure": + key = ("procedure", unit.name.lower()) + elif unit.kind in {"module", "submodule", "program", "block_data", "derived_type", "interface"}: + key = (unit.kind, unit.name.lower()) + else: + continue + for existing in seen.get(key, []): + if not _preprocessor_conditions_overlap(existing.condition_set, unit.condition_set): + continue + if unit.kind == "procedure": + scope_label = ( + f"module '{parent_scope.name}'" + if parent_scope.kind in {"module", "submodule"} + else "global scope" + ) + raise FortranParseError( + f"Duplicate procedure name '{unit.name}' in {scope_label}.", + filename=filename, + line_number=unit.start_line, + source_line=unit.lines[0][2] if unit.lines else None, + ) + raise FortranParseError( + f"Duplicate {unit.kind.replace('_', ' ')} name '{unit.name}' in {parent_scope.kind} scope.", + filename=filename, + line_number=unit.start_line, + source_line=unit.lines[0][2] if unit.lines else None, + ) + seen.setdefault(key, []).append(unit) + def _helper_split_unit_parts(self, unit: _SourceUnit, grammar: _UnitGrammar) -> _UnitParts: """Split one unit substring into grammar regions. @@ -2274,10 +2254,16 @@ def _helper_split_unit_parts(self, unit: _SourceUnit, grammar: _UnitGrammar) -> index = child_end + 1 continue + is_spec_statement = ( + _parse_use_statement(stripped) is not None + or _is_ignored_spec_statement(stripped) + or _looks_like_declaration_or_spec(stripped) + ) if ( region == "specification" and grammar.has_execution_part and _is_executable_statement_start(stripped) + and not is_spec_statement ): region = "execution" @@ -2315,6 +2301,30 @@ def _helper_validate_unit_headers(self, lines: _PreprocessedLines, filename: str if not stripped: continue self._parse_module_header(stripped, filename, lineno=lineno, source_line=source_line) + if stripped.lower().startswith("end "): + continue + if re.match(r"^module\s+procedure\s*::", stripped, flags=re.IGNORECASE): + continue + if not ( + stripped.lower().startswith("module procedure") + or _looks_like_procedure_header(stripped) + ): + continue + if self._parse_procedure_header( + stripped, + None, + False, + filename=filename, + lineno=lineno, + source_line=source_line, + ) is None: + self._raise_if_unparsed_procedure_header( + stripped, + in_interface=False, + filename=filename, + lineno=lineno, + source_line=source_line, + ) def _helper_scope_for_model( self, @@ -2350,20 +2360,6 @@ def _helper_scope_for_model( state=state, ) - def _track_containing_module_scope(self, current_module: str | None, line: str) -> tuple[str | None, bool]: - """Track module/submodule ownership for nested type/interface passes.""" - lower = line.lower() - submodule_match = _SUBMODULE_RE.match(line) - if submodule_match: - return submodule_match.group("name"), True - if lower.startswith("end submodule"): - return None, True - if self._is_module_program_unit_start(line): - return line.split()[1], True - if lower.startswith("end module"): - return None, True - return current_module, False - def _parse_module_header( self, line: str, @@ -2406,11 +2402,6 @@ def _collect_module_spec_line( return self._parse_module_variable_line(line, module, filename, lineno=lineno, source_line=source_line) - def _finalize_module(self, module: FortranModule, modules: list[FortranModule], filename: str | None) -> None: - _validate_module_variables(module, filename) - _apply_module_visibility(module, filename) - modules.append(module) - def _attach_module_children( self, modules: list[FortranModule], @@ -2464,15 +2455,6 @@ def _collect_submodule_spec_line( return self._parse_module_variable_line(line, submodule, filename, lineno=lineno, source_line=source_line) - def _finalize_submodule( - self, - submodule: FortranSubmodule, - submodules: list[FortranSubmodule], - filename: str | None, - ) -> None: - _validate_module_variables(submodule, filename) - submodules.append(submodule) - def _attach_submodule_children( self, submodules: list[FortranSubmodule], @@ -2520,15 +2502,6 @@ def _collect_program_spec_line( return self._parse_module_variable_line(line, program, filename, lineno=lineno, source_line=source_line) - def _finalize_program(self, program: FortranProgram, programs: list[FortranProgram], filename: str | None) -> None: - _validate_variable_declarations( - program.variables, - owner_kind="program", - owner_name=program.name, - filename=filename, - ) - programs.append(program) - def _parse_block_data_header(self, line: str, filename: str | None) -> FortranBlockData | None: match = _BLOCK_DATA_RE.match(line) if not match: @@ -2546,20 +2519,6 @@ def _collect_block_data_line( ) -> None: self._parse_module_variable_line(line, block_data, filename, lineno=lineno, source_line=source_line) - def _finalize_block_data( - self, - block_data: FortranBlockData, - blocks: list[FortranBlockData], - filename: str | None, - ) -> None: - _validate_variable_declarations( - block_data.variables, - owner_kind="block data", - owner_name=block_data.name, - filename=filename, - ) - blocks.append(block_data) - # ------------------------------------------------------------------ # High-level unit parsing (largest scopes first) # ------------------------------------------------------------------ @@ -2704,6 +2663,7 @@ def visit_module_unit( self._helper_visit_spec_part(scope, parts.specification, filename=filename) child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope) + self._helper_validate_sibling_units(child_units, parent_scope=scope, filename=filename) if signatures is None: signatures = [ self.visit_procedure_unit(child, parent_scope=scope, filename=filename) @@ -2752,6 +2712,7 @@ def visit_submodule_unit( self._helper_visit_spec_part(scope, parts.specification, filename=filename) child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope) + self._helper_validate_sibling_units(child_units, parent_scope=scope, filename=filename) if signatures is None: signatures = [ self.visit_procedure_unit(child, parent_scope=scope, filename=filename) @@ -2909,11 +2870,9 @@ def visit_procedure_unit( scope = self._helper_scope_for_model("procedure", proc_state["signature"], parent=parent_scope, state=proc_state) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("procedure")) self._helper_visit_spec_part(scope, parts.specification, filename=filename) + self._helper_apply_local_interface_declarations(proc_state, unit, scope, filename=filename) return _finalize_proc(proc_state) - def _parse_fortran_types(self, code: _SourceOrLines, filename: str | None = None) -> list[FortranDerivedType]: # pragma: no cover - private compatibility shim. - return self.visit_fortran_types(code, filename=filename) - def _handle_procedure_preprocessor_line( self, line: str, @@ -2977,225 +2936,90 @@ def _handle_procedure_preprocessor_line( def _procedure_preprocessor_condition_set(pp_condition_stack: list[tuple[int, int]]) -> frozenset[str]: return frozenset(f"g{group_id}:b{branch_id}" for group_id, branch_id in pp_condition_stack) - def _handle_procedure_interface_boundary( - self, - line: str, - *, - interface_depth: int, - interface_name_stack: list[str | None], - ) -> tuple[int, bool]: - lower = line.lower() - if lower.startswith("interface") or lower.startswith("abstract interface"): - parts = line.split(maxsplit=1) - iface_name = parts[1].strip() if len(parts) > 1 and not lower.startswith("abstract interface") else None - interface_name_stack.append(iface_name) - return interface_depth + 1, True - if lower.startswith("end interface"): - if interface_name_stack: - interface_name_stack.pop() - return max(0, interface_depth - 1), True - return interface_depth, False - - def _handle_procedure_owner_boundary( - self, + @staticmethod + def _raise_if_unparsed_procedure_header( line: str, *, - current_module: str | None, - current_module_uses: dict[str, list[FortranUseMapping]], - program_depth: int, - ) -> tuple[str | None, dict[str, list[FortranUseMapping]], int, bool]: - lower = line.lower() - submodule_match = _SUBMODULE_RE.match(line) - if submodule_match: - return submodule_match.group("name"), {}, program_depth, True - if lower.startswith("end submodule"): - return None, {}, program_depth, True - if _PROGRAM_RE.match(line): - return current_module, current_module_uses, program_depth + 1, True - if lower.startswith("end program"): - return current_module, current_module_uses, max(0, program_depth - 1), True - if self._is_module_program_unit_start(line): - return line.split()[1], {}, program_depth, True - if lower.startswith("end module"): - return None, {}, program_depth, True - return current_module, current_module_uses, program_depth, False - - def _register_procedure_block_name( - self, - proc_state: dict, - *, - current_module: str | None, - interface_depth: int, - declared_procedures: dict[tuple[str | None, bool], dict[str, list[frozenset[str]]]], - pp_condition_stack: list[tuple[int, int]], + in_interface: bool, filename: str | None, lineno: int | None, source_line: str | None, ) -> None: - scope_key = (current_module.lower() if current_module else None, interface_depth > 0) - seen_in_scope = declared_procedures.setdefault(scope_key, {}) - proc_name = proc_state["signature"].name.lower() - condition_set = self._procedure_preprocessor_condition_set(pp_condition_stack) - existing_conditions = seen_in_scope.setdefault(proc_name, []) - if ( - not interface_depth - and any(_preprocessor_conditions_overlap(existing, condition_set) for existing in existing_conditions) - ): - scope_label = f"module '{current_module}'" if current_module is not None else "global scope" + stripped = line.strip() + if not stripped: + return + lowered = stripped.lower() + if lowered.startswith("module procedure"): + if in_interface or _MOD_PROC_IMPL_RE.match(stripped) or "(" not in stripped: + return + raise FortranParseError( + f"Unsupported or malformed module procedure header: {stripped}", + filename=filename, + line_number=lineno, + source_line=source_line, + ) + if _looks_like_procedure_header(stripped): raise FortranParseError( - f"Duplicate procedure name '{proc_state['signature'].name}' in {scope_label}.", + f"Unsupported or malformed procedure header: {stripped}", filename=filename, line_number=lineno, source_line=source_line, ) - existing_conditions.append(condition_set) - def _start_procedure_block( + def _helper_apply_local_interface_declarations( self, - line: str, + proc_state: dict, + unit: _SourceUnit, + scope: _ParserScope, *, - current_module: str | None, - current_module_uses: dict[str, list[FortranUseMapping]], - interface_depth: int, - declared_procedures: dict[tuple[str | None, bool], dict[str, list[frozenset[str]]]], - pp_condition_stack: list[tuple[int, int]], filename: str | None, - lineno: int | None, - source_line: str | None, - ) -> dict | None: - proc_state = self._parse_procedure_header( - line, - current_module, - interface_depth > 0, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - if proc_state is None: - self._raise_if_unparsed_procedure_header( - line, - in_interface=interface_depth > 0, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - return None - self._register_procedure_block_name( - proc_state, - current_module=current_module, - interface_depth=interface_depth, - declared_procedures=declared_procedures, - pp_condition_stack=pp_condition_stack, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - proc_state["uses"].update(current_module_uses) - proc_state["filename"] = filename - proc_state["header_lineno"] = lineno - proc_state["header_source_line"] = source_line - proc_state["in_exec_part"] = False - return proc_state - - def _finalize_procedure_block( - self, - proc_state: dict, - signatures: list[FortranProcedureSignature], ) -> None: - signatures.append(_finalize_proc(proc_state)) - - def _handle_procedure_contains_line( - self, - line: str, - *, - current_proc: dict, - signatures: list[FortranProcedureSignature], - ) -> dict | None: - lower = line.lower() - if lower.startswith("end subroutine") or lower.startswith("end function") or lower.startswith("end procedure"): - end_parts = lower.split() - end_name = end_parts[2] if len(end_parts) > 2 else None - if end_name == current_proc["signature"].name.lower(): - self._finalize_procedure_block(current_proc, signatures) - return None - return current_proc - - def _handle_interface_procedure_declaration_line( - self, - line: str, - *, - current_proc: dict, - current_module: str | None, - signatures: list[FortranProcedureSignature], - filename: str | None, - lineno: int | None, - source_line: str | None, - ) -> dict | None: - lower = line.lower() - if ( - current_proc["signature"].in_interface - and ( - lower.startswith("end subroutine") - or lower.startswith("end function") - or lower.startswith("end procedure") - ) - ): - self._finalize_procedure_block(current_proc, signatures) - return None - if _looks_like_procedure_header(line): - iface_proc = self._parse_procedure_header( - line, - current_module, - True, - filename=filename, - lineno=lineno, - source_line=source_line, + """Mark dummy arguments declared by procedure-local interface blocks.""" + interface_units = self._helper_slice_child_units( + unit.lines[1:-1], + parent_scope=scope, + allowed_kinds={"interface"}, + ) + for interface_unit in interface_units: + interface_scope = _ParserScope( + kind="interface", + name=interface_unit.name, + parent=scope, + module_owner=scope.module_owner, ) - if iface_proc: - iface_name = iface_proc["signature"].name.lower() - self._proc_scope_mark_declared_symbol( - current_proc, - iface_name, + for child in self._helper_slice_child_units( + interface_unit.lines[1:-1], + parent_scope=interface_scope, + allowed_kinds={"procedure"}, + ): + header = child.lines[0] if child.lines else None + if header is None: + continue + parsed = self._parse_procedure_header( + header[0].strip(), + scope.module_owner, + True, filename=filename, - line_number=lineno, - source_line=source_line, + lineno=header[1], + source_line=header[2], ) - arg = self._proc_scope_get_symbol(current_proc, iface_name) - if arg is not None: + if parsed is None: + continue + name = parsed["signature"].name + if self._proc_scope_symbol_is_declared(proc_state, name): + key = self._scope_key(name) + else: + key = self._proc_scope_mark_declared_symbol( + proc_state, + name, + filename=filename, + line_number=header[1], + source_line=header[2], + ) + arg = self._proc_scope_get_symbol(proc_state, key) + if arg is not None and arg.base_type == "unknown": arg.base_type = "procedure" arg.kind = "" - return current_proc - return current_proc - - @staticmethod - def _raise_if_unparsed_procedure_header( - line: str, - *, - in_interface: bool, - filename: str | None, - lineno: int | None, - source_line: str | None, - ) -> None: - stripped = line.strip() - if not stripped: - return - lowered = stripped.lower() - if lowered.startswith("module procedure"): - if in_interface or _MOD_PROC_IMPL_RE.match(stripped): - return - raise FortranParseError( - f"Unsupported or malformed module procedure header: {stripped}", - filename=filename, - line_number=lineno, - source_line=source_line, - ) - if _looks_like_procedure_header(stripped): - raise FortranParseError( - f"Unsupported or malformed procedure header: {stripped}", - filename=filename, - line_number=lineno, - source_line=source_line, - ) def _helper_apply_meta_to_variable(self, var: FortranVariable, meta: dict, shape: list[str]) -> None: """Apply parsed declaration metadata to a variable-like model. @@ -3276,6 +3100,7 @@ def _helper_push_declaration_to_scope( raise FortranParseError("Declaration scope is missing a target model.", filename=filename) for entity in split_csv(right): + initializer = entity.split("=", 1)[1].strip() if "=" in entity else None raw_name, shape = _var(entity) if not raw_name: continue @@ -3289,6 +3114,9 @@ def _helper_push_declaration_to_scope( continue var = FortranArgument(name=normalized_name) _apply(var, meta, shape) + if initializer is not None and meta["parameter"]: + var.value = _normalize_parameter_value(initializer) + var.value_type = "expression" target.variables.append(var) def _helper_parse_declaration_line( @@ -3542,181 +3370,6 @@ def _handle_unknown_proc_declaration( source_line=source_line, ) - def _apply_declaration_to_procedure_state( - self, - meta: dict, - right: str, - proc_state: dict, - *, - filename: str | None, - lineno: int | None, - source_line: str | None, - ) -> None: - self._helper_push_declaration_to_scope( - _ParserScope( - kind="procedure", - name=proc_state["signature"].name, - model=proc_state["signature"], - state=proc_state, - module_owner=proc_state["signature"].module, - ), - meta=meta, - right=right, - role="procedure_symbol", - filename=filename, - lineno=lineno, - source_line=source_line, - ) - - def _collect_procedure_signatures( - self, - code: _SourceOrLines, - filename: str | None = None, - macro_defines: set[str] | dict[str, int | bool | str] | None = None, - ) -> list[FortranProcedureSignature]: - """Scan source lines for procedure blocks and collect finalized signatures. - - Scope model: - - Tracks module/submodule scope (`current_module`). - - Tracks interface nesting (`interface_depth`) to mark interface members. - - Tracks preprocessor branches and suppresses inactive blocks when macro - selection is enabled. - - Identifies procedure block boundaries from `subroutine/function` headers - to matching `end ...` statements, then finalizes symbols/types/shapes. - """ - lines = self._preprocessed_lines(code, filename) - macro_selection_enabled = macro_defines is not None - signatures: list[FortranProcedureSignature] = [] - declared_procedures: dict[tuple[str | None, bool], dict[str, list[frozenset[str]]]] = {} - current_module = None - current_module_uses: dict[str, list[FortranUseMapping]] = {} - current_proc = None - interface_depth = 0 - interface_name_stack: list[str | None] = [] - program_depth = 0 - macro_names = _normalize_macro_defines(macro_defines) - pp_condition_stack: list[tuple[int, int]] = [] - pp_active_stack: list[bool] = [] - pp_group_counter = 0 - - for line, lineno, source_line in lines: - s = line.strip() - if not s: - continue - - handled_pp, pp_group_counter = self._handle_procedure_preprocessor_line( - s, - macro_selection_enabled=macro_selection_enabled, - macro_names=macro_names, - pp_condition_stack=pp_condition_stack, - pp_active_stack=pp_active_stack, - pp_group_counter=pp_group_counter, - ) - if handled_pp: - continue - - if macro_selection_enabled and pp_active_stack and not all(pp_active_stack): - continue - - l = s.lower() - - interface_depth, handled_interface = self._handle_procedure_interface_boundary( - s, - interface_depth=interface_depth, - interface_name_stack=interface_name_stack, - ) - if handled_interface: - continue - - current_module, current_module_uses, program_depth, handled_owner = self._handle_procedure_owner_boundary( - s, - current_module=current_module, - current_module_uses=current_module_uses, - program_depth=program_depth, - ) - if handled_owner: - continue - if program_depth > 0: - continue - - if current_proc is None: - parsed_use = _parse_use_statement(s) - if parsed_use and current_module is not None: - module_name, mappings = parsed_use - current_module_uses[module_name] = mappings - continue - - if current_proc is None: - current_proc = self._start_procedure_block( - s, - current_module=current_module, - current_module_uses=current_module_uses, - interface_depth=interface_depth, - declared_procedures=declared_procedures, - pp_condition_stack=pp_condition_stack, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - continue - - if current_proc is not None and current_proc.get("in_contains"): - current_proc = self._handle_procedure_contains_line( - s, - current_proc=current_proc, - signatures=signatures, - ) - continue - if current_proc is not None and interface_depth > 0: - current_proc = self._handle_interface_procedure_declaration_line( - s, - current_proc=current_proc, - current_module=current_module, - signatures=signatures, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - continue - - if l.startswith("end subroutine") or l.startswith("end function") or l.startswith("end procedure") or l == "end": - self._finalize_procedure_block(current_proc, signatures) - current_proc = None - continue - if self._is_contains_transition(s): - current_proc["in_contains"] = True - continue - if current_proc.get("in_contains"): - continue - - if re.match(r"^type\s+\w+$", s, flags=re.IGNORECASE): - current_proc["local_type_depth"] = current_proc.get("local_type_depth", 0) + 1 - continue - if current_proc.get("local_type_depth", 0) > 0: - if re.match(r"^end\s+type\b", s, flags=re.IGNORECASE): - current_proc["local_type_depth"] -= 1 - continue - - parsed_use = _parse_use_statement(s) - if parsed_use: - module_name, mappings = parsed_use - current_proc["uses"][module_name] = mappings - continue - - if current_proc.get("in_exec_part"): - continue - - if _is_executable_statement_start(s): - current_proc["in_exec_part"] = True - continue - - self._parse_procedure_declaration_line(s, current_proc, filename=filename, lineno=lineno, source_line=source_line) - - if current_proc is not None: - self._finalize_procedure_block(current_proc, signatures) - - return signatures - def _parse_procedure_declaration_line(self, line: str, proc_state: dict, filename: str | None = None, lineno: int | None = None, source_line: str | None = None) -> None: """Parse one declaration/specification line inside a procedure scope. @@ -3738,6 +3391,11 @@ def _parse_procedure_declaration_line(self, line: str, proc_state: dict, filenam return if self._handle_proc_external_line(stripped, proc_state): return + parsed_use = _parse_use_statement(stripped) + if parsed_use: + module_name, mappings = parsed_use + proc_state["uses"][module_name] = mappings + return # This parser is a subset parser focused on wrapper-relevant metadata. # These statements do not affect extracted signature typing/shapes. if self._is_ignored_proc_spec_line(stripped): @@ -3863,36 +3521,6 @@ def _parse_module_variable_line(self, line: str, module: FortranModule | Fortran lineno=lineno, source_line=source_line, ) - def _parse_single_procedure_signature( - self, - code: _SourceOrLines, - filename: str | None = None, - macro_defines: set[str] | dict[str, int | bool | str] | None = None, - ) -> FortranProcedureSignature: - return _expect_single_parse_result( - self._collect_procedure_signatures(code, filename=filename, macro_defines=macro_defines), - parser_name="parse_single_procedure_signature", - entity_name="procedure signature", - filename=filename, - ) - - def _collect_project_procedure_signatures(self, files: dict[str, str]) -> list[FortranProcedureSignature]: - module_params: dict[str, dict[str, str]] = {} - parsed_files: list[tuple[str, list[FortranProcedureSignature]]] = [] - preprocessed_files = {fname: preprocess_lines(code, fname) for fname, code in files.items()} - for fname, lines in preprocessed_files.items(): - module_params.update(self._collect_module_parameters(lines, fname)) - parsed_files.append((fname, self._collect_procedure_signatures(lines, filename=fname))) - - for _, signatures in parsed_files: - for sig in signatures: - _resolve_signature_kinds(sig, module_params) - - out: list[FortranProcedureSignature] = [] - for _, signatures in parsed_files: - out.extend(signatures) - return out - def _collect_module_parameters(self, code: _SourceOrLines, filename: str | None) -> dict[str, dict[str, str]]: lines = self._preprocessed_lines(code, filename) current_module = None @@ -3951,54 +3579,70 @@ def visit_fortran_file( code = str(source) effective_macro_defines = self.macro_defines if macro_defines is None else macro_defines - lines = preprocess_lines(code, filename) + raw_lines = preprocess_lines(code, filename) + lines = self._helper_select_active_preprocessor_lines(raw_lines, effective_macro_defines) self._helper_validate_unit_headers(lines, filename) root_scope = _ParserScope(kind="file", name=None) top_units = self._helper_slice_child_units(lines, parent_scope=root_scope) - signatures = self._collect_procedure_signatures(lines, filename=filename, macro_defines=effective_macro_defines) - derived_types = self.visit_fortran_types(lines, filename=filename) - interfaces = self.visit_fortran_interfaces(lines, filename=filename) - modules = [ - self.visit_module_unit( - unit, - parent_scope=root_scope, - filename=filename, - signatures=signatures, - types=derived_types, - interfaces=interfaces, - ) - for unit in top_units - if unit.kind == "module" - ] - submodules = [ - self.visit_submodule_unit( - unit, - parent_scope=root_scope, - filename=filename, - signatures=signatures, - types=derived_types, - interfaces=interfaces, - ) - for unit in top_units - if unit.kind == "submodule" - ] - programs = [ - self.visit_program_unit(unit, parent_scope=root_scope, filename=filename) - for unit in top_units - if unit.kind == "program" - ] - block_data_units = [ - self.visit_block_data_source_unit(unit, parent_scope=root_scope, filename=filename) - for unit in top_units - if unit.kind == "block_data" + self._helper_validate_sibling_units(top_units, parent_scope=root_scope, filename=filename) + modules: list[FortranModule] = [] + submodules: list[FortranSubmodule] = [] + programs: list[FortranProgram] = [] + block_data_units: list[FortranBlockData] = [] + standalone_procedures: list[FortranProcedureSignature] = [] + interfaces: list[FortranInterface] = [] + derived_types: list[FortranDerivedType] = [] + + for unit in top_units: + parsed_unit = self.visit_source_unit(unit, parent_scope=root_scope, filename=filename) + if isinstance(parsed_unit, FortranModule): + modules.append(parsed_unit) + elif isinstance(parsed_unit, FortranSubmodule): + submodules.append(parsed_unit) + elif isinstance(parsed_unit, FortranProgram): + programs.append(parsed_unit) + elif isinstance(parsed_unit, FortranBlockData): + block_data_units.append(parsed_unit) + elif isinstance(parsed_unit, FortranProcedureSignature): + if not parsed_unit.in_interface: + standalone_procedures.append(parsed_unit) + elif isinstance(parsed_unit, FortranInterface): + interfaces.append(parsed_unit) + elif isinstance(parsed_unit, FortranDerivedType): + derived_types.append(parsed_unit) + + all_types = [ + *derived_types, + *(dtype for module in modules for dtype in module.derived_types), + *(dtype for submodule in submodules for dtype in submodule.derived_types), ] + self._resolve_derived_type_extensions(all_types) + all_interfaces = self.visit_fortran_interfaces(lines, filename=filename) + interfaces = [iface for iface in all_interfaces if iface.module is None] + for module in modules: + module.interfaces = [ + iface for iface in all_interfaces + if iface.module and iface.module.lower() == module.name.lower() + ] + for submodule in submodules: + submodule.interfaces = [ + iface for iface in all_interfaces + if iface.module and iface.module.lower() == submodule.name.lower() + ] - owned_proc_ids = {id(proc) for mod in modules for proc in mod.procedures} - owned_proc_ids.update(id(proc) for submod in submodules for proc in submod.procedures) - standalone_procedures = [ - sig for sig in signatures - if sig.module is None and not sig.in_interface and id(sig) not in owned_proc_ids - ] + variable_units = [*modules, *submodules, *programs, *block_data_units] + module_params = self._collect_module_parameters(lines, filename) + if any(var.kind or var.value is not None for unit in variable_units for var in getattr(unit, "variables", [])): + for unit in variable_units: + _resolve_module_variable_kinds(unit, module_params) + for proc in standalone_procedures: + _resolve_signature_kinds(proc, module_params, resolve_shapes=False) + for module in modules: + for proc in module.procedures: + _resolve_signature_kinds(proc, module_params, resolve_shapes=False) + for submodule in submodules: + for proc in submodule.procedures: + _resolve_signature_kinds(proc, module_params, resolve_shapes=False) file = FortranFile( filename=filename, @@ -4022,20 +3666,6 @@ def visit_fortran_file( self._insert_unique_scope_symbol(file.symbols, p.name.lower(), p, label="file scope", filename=filename) return file - def parse_fortran_file( - self, - source: str | Path, - filename: str | None = None, - macro_defines: set[str] | dict[str, int | bool | str] | None = None, - encoding: str = "utf-8", - ) -> FortranFile: - return self.visit_fortran_file( - source, - filename=filename, - macro_defines=macro_defines, - encoding=encoding, - ) - def visit_fortran_project( self, files: dict[str, str] | list[str | Path] | tuple[str | Path, ...] | str | Path, @@ -4046,11 +3676,33 @@ def visit_fortran_project( if isinstance(files, dict): parsed_files = [self.visit_file(code, filename=fname, encoding=encoding) for fname, code in files.items()] elif isinstance(files, (str, Path)): - namespace = self._parse_fortran_namespace(files) + namespace = self._helper_collect_namespace(files) parsed_files = [self.visit_file(path, encoding=encoding) for path in namespace["files"]] else: parsed_files = [self.visit_file(path, encoding=encoding) for path in files] + module_params: dict[str, dict[str, str]] = {} + for parsed_file in parsed_files: + if parsed_file.source is not None: + module_params.update(self._collect_module_parameters(parsed_file.source, parsed_file.filename)) + + seen_procedures: set[int] = set() + for parsed_file in parsed_files: + for proc in parsed_file.procedures: + if id(proc) not in seen_procedures: + _resolve_signature_kinds(proc, module_params, resolve_shapes=False) + seen_procedures.add(id(proc)) + for module in parsed_file.modules: + for proc in module.procedures: + if id(proc) not in seen_procedures: + _resolve_signature_kinds(proc, module_params, resolve_shapes=False) + seen_procedures.add(id(proc)) + for submodule in parsed_file.submodules: + for proc in submodule.procedures: + if id(proc) not in seen_procedures: + _resolve_signature_kinds(proc, module_params, resolve_shapes=False) + seen_procedures.add(id(proc)) + project = FortranProject(files=parsed_files) for f in parsed_files: @@ -4095,9 +3747,6 @@ def visit_fortran_project( self._insert_unique_scope_symbol(project.interfaces, iface.name.lower(), iface, label="project interface scope") return project - def parse_fortran_project(self, files, *, encoding: str = "utf-8") -> FortranProject: - return self.visit_fortran_project(files, encoding=encoding) - # ------------------------------------------------------------------ # Derived types, modules, interfaces, and other program units # ------------------------------------------------------------------ @@ -4165,15 +3814,6 @@ def _parse_derived_type_contains_line( source_line=source_line, ) - def _finalize_derived_type( - self, - dtype: FortranDerivedType, - types: list[FortranDerivedType], - filename: str | None, - ) -> None: - _validate_derived_type_fields(dtype, filename) - types.append(dtype) - @staticmethod def _resolve_derived_type_extensions(types: list[FortranDerivedType]) -> None: by_name = {t.name.lower(): t for t in types} @@ -4305,22 +3945,12 @@ def _helper_visit_fortran_modules( for line, lineno, source_line in lines: if line.strip(): _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) - signatures = self._collect_procedure_signatures(code, filename) if signatures is None else signatures - types = self._parse_fortran_types(code, filename) if types is None else types - interfaces = self._parse_fortran_interfaces(code, filename) if interfaces is None else interfaces root_scope = _ParserScope(kind="file", name=None) - modules = [ - self.visit_module_unit( - unit, - parent_scope=root_scope, - filename=filename, - signatures=signatures, - types=types, - interfaces=interfaces, - ) - for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"module"}) - ] - if require_present and not modules and signatures: + all_units = self._helper_slice_child_units(lines, parent_scope=root_scope) + module_units = [unit for unit in all_units if unit.kind == "module"] + self._helper_validate_sibling_units(module_units, parent_scope=root_scope, filename=filename) + modules = [self.visit_module_unit(unit, parent_scope=root_scope, filename=filename) for unit in module_units] + if require_present and not modules and any(unit.kind == "procedure" for unit in all_units): raise FortranParseError( "visit_fortran_modules() expected a module program unit, but only standalone procedures were found", filename=filename, @@ -4345,19 +3975,6 @@ def _add_interface_attribute(sig: FortranProcedureSignature, interface_name: str if iface_attr not in sig.attributes: sig.attributes.append(iface_attr) - def _finalize_interface_procedure( - self, - proc_state: dict, - interface: FortranInterface, - ) -> None: - sig = _finalize_proc(proc_state) - self._add_interface_attribute(sig, interface.name) - interface.procedures.append(sig) - - @staticmethod - def _finalize_interface(interface: FortranInterface, interfaces: list[FortranInterface]) -> None: - interfaces.append(interface) - def _helper_visit_fortran_interfaces(self, code: _SourceOrLines, filename: str | None = None) -> list[FortranInterface]: """Parse `interface ... end interface` blocks and their procedure members. @@ -4403,23 +4020,6 @@ def collect(scope: _ParserScope, source_lines: _PreprocessedLines) -> None: return interfaces - def _parse_interface_procedure_header( - self, - line: str, - module: str | None, - filename: str | None = None, - lineno: int | None = None, - source_line: str | None = None, - ): - return self._parse_procedure_header( - line, - module, - True, - filename=filename, - lineno=lineno, - source_line=source_line, - ) - def _parse_procedure_header( self, line: str, @@ -4517,21 +4117,10 @@ def _helper_visit_fortran_submodules( for line, lineno, source_line in lines: if line.strip(): _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) - signatures = self._collect_procedure_signatures(code, filename) if signatures is None else signatures - types = self._parse_fortran_types(code, filename) if types is None else types - interfaces = self._parse_fortran_interfaces(code, filename) if interfaces is None else interfaces root_scope = _ParserScope(kind="file", name=None) - submodules = [ - self.visit_submodule_unit( - unit, - parent_scope=root_scope, - filename=filename, - signatures=signatures, - types=types, - interfaces=interfaces, - ) - for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"submodule"}) - ] + submodule_units = self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"submodule"}) + self._helper_validate_sibling_units(submodule_units, parent_scope=root_scope, filename=filename) + submodules = [self.visit_submodule_unit(unit, parent_scope=root_scope, filename=filename) for unit in submodule_units] return submodules def _helper_visit_fortran_programs(self, code: _SourceOrLines, filename: str | None = None) -> list[FortranProgram]: @@ -4548,18 +4137,9 @@ def _helper_visit_fortran_programs(self, code: _SourceOrLines, filename: str | N if line.strip(): _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) root_scope = _ParserScope(kind="file", name=None) - return [ - self.visit_program_unit(unit, parent_scope=root_scope, filename=filename) - for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"program"}) - ] - - def _parse_fortran_program(self, code: _SourceOrLines, filename: str | None = None) -> FortranProgram: # pragma: no cover - private compatibility shim. - return _expect_single_parse_result( - self._parse_fortran_programs(code, filename=filename), - parser_name="parse_fortran_program", - entity_name="program", - filename=filename, - ) + program_units = self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"program"}) + self._helper_validate_sibling_units(program_units, parent_scope=root_scope, filename=filename) + return [self.visit_program_unit(unit, parent_scope=root_scope, filename=filename) for unit in program_units] def _helper_visit_fortran_block_data(self, code: _SourceOrLines, filename: str | None = None) -> list[FortranBlockData]: """Parse `block data` units and their declaration sections.""" @@ -4569,18 +4149,9 @@ def _helper_visit_fortran_block_data(self, code: _SourceOrLines, filename: str | if line.strip(): _enforce_source_form_compatibility(line.strip(), filename, lineno, source_line) root_scope = _ParserScope(kind="file", name=None) - return [ - self.visit_block_data_source_unit(unit, parent_scope=root_scope, filename=filename) - for unit in self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"block_data"}) - ] - - def _parse_fortran_block_data_unit(self, code: _SourceOrLines, filename: str | None = None) -> FortranBlockData: # pragma: no cover - private compatibility shim. - return _expect_single_parse_result( - self._parse_fortran_block_data(code, filename=filename), - parser_name="parse_fortran_block_data_unit", - entity_name="block data unit", - filename=filename, - ) + block_units = self._helper_slice_child_units(lines, parent_scope=root_scope, allowed_kinds={"block_data"}) + self._helper_validate_sibling_units(block_units, parent_scope=root_scope, filename=filename) + return [self.visit_block_data_source_unit(unit, parent_scope=root_scope, filename=filename) for unit in block_units] def _helper_collect_namespace( self, @@ -4623,35 +4194,20 @@ def _helper_collect_namespace( file_dependencies[fname] = deps ordered_files = _topological_files(file_dependencies) - signatures = self._collect_project_procedure_signatures({f: sources[f] for f in ordered_files}) types = [] modules = [] submodules = [] programs = [] block_data = [] for f in ordered_files: - lines = file_lines[f] - file_types = self.visit_fortran_types(lines, filename=f) - file_interfaces = self.visit_fortran_interfaces(lines, filename=f) - file_signatures = self._collect_procedure_signatures(lines, filename=f) - types.extend(file_types) - modules.extend(self.visit_fortran_modules( - lines, - filename=f, - require_present=False, - signatures=file_signatures, - types=file_types, - interfaces=file_interfaces, - )) - submodules.extend(self.visit_fortran_submodules( - lines, - filename=f, - signatures=file_signatures, - types=file_types, - interfaces=file_interfaces, - )) - programs.extend(self.visit_fortran_programs(lines, filename=f)) - block_data.extend(self.visit_fortran_block_data(lines, filename=f)) + parsed_file = self.visit_file(sources[f], filename=f) + types.extend(parsed_file.derived_types) + types.extend(dtype for module in parsed_file.modules for dtype in module.derived_types) + types.extend(dtype for submodule in parsed_file.submodules for dtype in submodule.derived_types) + modules.extend(parsed_file.modules) + submodules.extend(parsed_file.submodules) + programs.extend(parsed_file.programs) + block_data.extend(parsed_file.block_data_units) return { "files": ordered_files, @@ -4663,31 +4219,32 @@ def _helper_collect_namespace( "programs": programs, "block_data": block_data, "types": types, - "signatures": signatures, } def visit_fortran_wrap_readiness(self, code: str, filename: str | None = None) -> dict: lines = self._preprocessed_lines(code, filename) - signatures = self._collect_procedure_signatures(lines, filename) - types = self.visit_fortran_types(lines, filename) - interfaces = self.visit_fortran_interfaces(lines, filename) - modules = self.visit_fortran_modules( - lines, - filename, - require_present=False, - signatures=signatures, - types=types, - interfaces=interfaces, - ) - submodules = self.visit_fortran_submodules( - lines, - filename, - signatures=signatures, - types=types, - interfaces=interfaces, - ) - programs = self.visit_fortran_programs(lines, filename) - block_data = self.visit_fortran_block_data(lines, filename) + parsed_file = self.visit_file(code, filename=filename) + modules = parsed_file.modules + submodules = parsed_file.submodules + programs = parsed_file.programs + block_data = parsed_file.block_data_units + interfaces = [ + *parsed_file.interfaces, + *(iface for module in modules for iface in module.interfaces), + *(iface for submodule in submodules for iface in submodule.interfaces), + ] + signatures = [ + *parsed_file.procedures, + *(proc for module in modules for proc in module.procedures), + *(proc for submodule in submodules for proc in submodule.procedures), + *(proc for iface in interfaces for proc in iface.procedures), + ] + types = [ + *parsed_file.derived_types, + *(dtype for module in modules for dtype in module.derived_types), + *(dtype for submodule in submodules for dtype in submodule.derived_types), + ] + wrap_target_signatures = [sig for sig in signatures if not sig.in_interface] unsupported: list[dict] = [] for line, lineno, source_line in lines: for p in _UNSUPPORTED_PATTERNS: @@ -4696,19 +4253,19 @@ def visit_fortran_wrap_readiness(self, code: str, filename: str | None = None) - break missing_decl_args: list[str] = [] - for sig in signatures: + for sig in wrap_target_signatures: for a in sig.arguments: if a.base_type == "unknown": missing_decl_args.append(f"{sig.name}:{a.name}") module_params = self._collect_module_parameters(code, filename) unresolved_derived_args, unresolved_derived_fields = _collect_unresolved_derived_type_diagnostics( - signatures, + wrap_target_signatures, types, modules, ) unresolved_kind_args, unresolved_kind_fields = _collect_unresolved_kind_diagnostics( - signatures, + wrap_target_signatures, types, modules, module_params, diff --git a/parser_implementation_reference.md b/parser_implementation_reference.md index a8a431fd8..ddaef5282 100644 --- a/parser_implementation_reference.md +++ b/parser_implementation_reference.md @@ -329,15 +329,22 @@ If you want another agent to reproduce the same architecture for a new language, ask it to implement each of these layers explicitly: 1. Preprocessor/lexer layer for source forms, comments, continuations. -2. Procedure signature parser (headers, args, attributes, result handling). -3. Declaration parser for types/intent/shape/flags. -4. Module/package parser with import/use tracking. -5. Composite type parser (fields, inheritance, bound methods/generics). -6. Interface/contract block parser. -7. Symbol resolver for local and cross-file compile-time constants. -8. Project namespace parser with dependency ordering. -9. Readiness validator with unsupported-pattern rules + unknown type checks. -10. CLI with tree output + JSON output + file emission. +2. Source-unit slicer that preserves original line numbers and returns direct + children for each scope. +3. Grammar-region splitter for `header`, specification part, execution part, + and `contains`. +4. Unit visitors for modules, submodules, programs, procedures, derived types, + interfaces, and block data. +5. Shared declaration parser for variables, procedure arguments/results, and + derived-type fields, with scope-specific storage handled separately. +6. Procedure signature parser (headers, args, attributes, result handling). +7. Module/package parser with import/use tracking. +8. Composite type parser (fields, inheritance, bound methods/generics). +9. Interface/contract block parser. +10. Symbol resolver for local and cross-file compile-time constants. +11. Project namespace parser with dependency ordering. +12. Readiness validator with unsupported-pattern rules + unknown type checks. +13. CLI with tree output + JSON output + file emission. 11. Unit tests per feature + fixture/golden regression suite + golden regeneration script. @@ -407,6 +414,13 @@ A condensed history of important parser capabilities added over time (from - Added parser public API coverage for less common module/import forms and preserved `use` rename source/target mappings through JSON, semantic IR, and `.pyi` import aliases. +- Refactored `FortranParser.visit_file` onto recursive source-unit slicing: + file parsing now dispatches direct units to small `visit_*_unit` methods, + each unit works on its own source substring, and shared declaration helpers + push parsed symbols into the active scope. +- Refreshed parser goldens for the grammar-style parser. Procedure-internal + subprograms are no longer exported as file/module procedures; local + interfaces remain available for callback typing and interface metadata. ## 9) Pull-request maintenance policy for this reference @@ -491,12 +505,15 @@ When updating parser behavior, keep this fail-fast contract aligned with tests: diagnostics where appropriate; unknown datatype syntax should crash early. - **Preprocessor-conditional duplicate procedures (guarded allowance):** - The parser does **not** run a full C preprocessor stage before parsing. - - While scanning signatures, simple directive structure is tracked for `#ifdef`, `#ifndef`, `#elif`, `#else`, and `#endif` to model mutually-exclusive branches. - - `visit_file(..., macro_defines=...)` can provide macro decisions; inactive conditional branches are skipped during signature extraction so the active code path is selected. The module-level `parse_fortran_file(...)` convenience function delegates to this visitor. + - While slicing source units, simple directive structure is tracked for + `#ifdef`, `#ifndef`, `#elif`, `#else`, and `#endif` to model + mutually-exclusive branches. + - `visit_file(..., macro_defines=...)` can provide macro decisions; inactive conditional branches are skipped before unit parsing so the active code path is selected. The module-level `parse_fortran_file(...)` convenience function delegates to this visitor. - accepted forms: `set[str]` or `dict[str, int|bool|str]` - dictionary values are truthy/falsey (`0`, `False`, `"0"`, `"false"` treated as undefined/disabled) - Basic `#if` expressions are supported for branch selection (`defined(X)`, `!`, `&&`, `||`, parentheses, `0`/`1`). - - Duplicate procedure-name checks in a module/global scope are evaluated against this branch context: + - Duplicate procedure-name checks in a module/global scope are evaluated + against same-level sliced units and this branch context: - if two same-name procedure headers are reachable in an overlapping branch context, raise `FortranParseError` (duplicate procedure name). - if they are only present in mutually-exclusive branches of the same conditional group, allow both signatures. - This is a structural exclusivity model (branch groups), not semantic evaluation of macro expressions. In other words, branch mutual exclusivity is honored without requiring expression truth evaluation. diff --git a/tests/parser/fortran/fixtures/scifortran/fmin_cg_minimize.json b/tests/parser/fortran/fixtures/scifortran/fmin_cg_minimize.json index a998806d7..e0617a475 100644 --- a/tests/parser/fortran/fixtures/scifortran/fmin_cg_minimize.json +++ b/tests/parser/fortran/fixtures/scifortran/fmin_cg_minimize.json @@ -225,87 +225,101 @@ "uses": {}, "in_interface": false, "variables": {} - }, + } + ], + "interfaces": [ { - "name": "fcn_", - "kind": "subroutine", + "name": null, "module": null, - "arguments": [ - { - "name": "n", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn_", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, + "procedures": [ { - "name": "x", - "base_type": "real", - "kind": "8", - "rank": 1, - "shape": [ - "n" - ], - "lbound": [ - "1" - ], - "ubound": [ - "n" + "name": "fcn", + "kind": "subroutine", + "module": null, + "arguments": [ + { + "name": "n", + "base_type": "integer", + "kind": "", + "rank": 0, + "shape": [], + "lbound": [], + "ubound": [], + "value": null, + "value_type": "unknown", + "is_parameter": false, + "dimensions": [], + "visibility": "public", + "procedure": "fcn", + "intent": "unknown", + "optional": false, + "pass_by_value": false, + "allocatable": false, + "pointer": false + }, + { + "name": "x_", + "base_type": "real", + "kind": "8", + "rank": 1, + "shape": [ + "n" + ], + "lbound": [ + "1" + ], + "ubound": [ + "n" + ], + "value": null, + "value_type": "unknown", + "is_parameter": false, + "dimensions": [], + "visibility": "public", + "procedure": "fcn", + "intent": "unknown", + "optional": false, + "pass_by_value": false, + "allocatable": false, + "pointer": false + }, + { + "name": "f_", + "base_type": "real", + "kind": "8", + "rank": 0, + "shape": [], + "lbound": [], + "ubound": [], + "value": null, + "value_type": "unknown", + "is_parameter": false, + "dimensions": [], + "visibility": "public", + "procedure": "fcn", + "intent": "unknown", + "optional": false, + "pass_by_value": false, + "allocatable": false, + "pointer": false + } ], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn_", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "f", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn_", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false + "result": null, + "attributes": [], + "uses": {}, + "in_interface": true, + "variables": {} } - ], - "result": null, - "attributes": [], - "uses": {}, - "in_interface": false, - "variables": {} - }, - { - "name": "fmin_cgminimize_sub", + ] + } + ], + "derived_types": [], + "variables": [], + "includes": [], + "diagnostics": [], + "symbols": { + "fmin_cgminimize_func": { + "name": "fmin_cgminimize_func", "kind": "subroutine", "module": null, "arguments": [ @@ -328,7 +342,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "inout", "optional": false, "pass_by_value": false, @@ -338,7 +352,7 @@ { "name": "fcn", "base_type": "procedure", - "kind": "", + "kind": "cgfit_func", "rank": 0, "shape": [], "lbound": [], @@ -348,7 +362,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": false, "pass_by_value": false, @@ -368,7 +382,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": false, "pass_by_value": false, @@ -388,7 +402,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": false, "pass_by_value": false, @@ -408,7 +422,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": true, "pass_by_value": false, @@ -428,7 +442,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": true, "pass_by_value": false, @@ -448,7 +462,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": true, "pass_by_value": false, @@ -468,7 +482,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": true, "pass_by_value": false, @@ -488,7 +502,7 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", + "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": true, "pass_by_value": false, @@ -508,618 +522,12 @@ "is_parameter": false, "dimensions": [], "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - } - ], - "result": null, - "attributes": [], - "uses": {}, - "in_interface": false, - "variables": {} - } - ], - "interfaces": [ - { - "name": null, - "module": null, - "procedures": [ - { - "name": "fcn", - "kind": "subroutine", - "module": null, - "arguments": [ - { - "name": "n", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "x_", - "base_type": "real", - "kind": "8", - "rank": 1, - "shape": [ - "n" - ], - "lbound": [ - "1" - ], - "ubound": [ - "n" - ], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "f_", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - } - ], - "result": null, - "attributes": [], - "uses": {}, - "in_interface": true, - "variables": {} - } - ] - } - ], - "derived_types": [], - "variables": [], - "includes": [], - "diagnostics": [], - "symbols": { - "fmin_cgminimize_func": { - "name": "fmin_cgminimize_func", - "kind": "subroutine", - "module": null, - "arguments": [ - { - "name": "p", - "base_type": "real", - "kind": "8", - "rank": 1, - "shape": [ - ":" - ], - "lbound": [ - null - ], - "ubound": [ - null - ], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "inout", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "fcn", - "base_type": "procedure", - "kind": "cgfit_func", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "iter", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "fret", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "ftol", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "itmax", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "iverbose", - "base_type": "logical", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "mode", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "new_version", - "base_type": "logical", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", "procedure": "fmin_cgminimize_func", "intent": "unknown", "optional": true, "pass_by_value": false, "allocatable": false, "pointer": false - }, - { - "name": "hh_par", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_func", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - } - ], - "result": null, - "attributes": [], - "uses": {}, - "in_interface": false, - "variables": {} - }, - "fcn_": { - "name": "fcn_", - "kind": "subroutine", - "module": null, - "arguments": [ - { - "name": "n", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn_", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "x", - "base_type": "real", - "kind": "8", - "rank": 1, - "shape": [ - "n" - ], - "lbound": [ - "1" - ], - "ubound": [ - "n" - ], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn_", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "f", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fcn_", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - } - ], - "result": null, - "attributes": [], - "uses": {}, - "in_interface": false, - "variables": {} - }, - "fmin_cgminimize_sub": { - "name": "fmin_cgminimize_sub", - "kind": "subroutine", - "module": null, - "arguments": [ - { - "name": "p", - "base_type": "real", - "kind": "8", - "rank": 1, - "shape": [ - ":" - ], - "lbound": [ - null - ], - "ubound": [ - null - ], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "inout", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "fcn", - "base_type": "procedure", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "iter", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "fret", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "ftol", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "itmax", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "iverbose", - "base_type": "logical", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "mode", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "new_version", - "base_type": "logical", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "hh_par", - "base_type": "real", - "kind": "8", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": "fmin_cgminimize_sub", - "intent": "unknown", - "optional": true, - "pass_by_value": false, - "allocatable": false, - "pointer": false } ], "result": null, diff --git a/tests/parser/fortran/fixtures/scifortran/integrate_quad_sample.json b/tests/parser/fortran/fixtures/scifortran/integrate_quad_sample.json index 088d5ed9e..c410e8f49 100644 --- a/tests/parser/fortran/fixtures/scifortran/integrate_quad_sample.json +++ b/tests/parser/fortran/fixtures/scifortran/integrate_quad_sample.json @@ -354,151 +354,7 @@ } ], "interfaces": [], - "derived_types": [ - { - "name": "finter_type", - "module": null, - "fields": [ - { - "name": "X", - "base_type": "real", - "kind": "8", - "rank": 1, - "shape": [ - ":" - ], - "lbound": [ - null - ], - "ubound": [ - null - ], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": null, - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": true, - "pointer": false - }, - { - "name": "F", - "base_type": "real", - "kind": "8", - "rank": 1, - "shape": [ - ":" - ], - "lbound": [ - null - ], - "ubound": [ - null - ], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": null, - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": true, - "pointer": false - }, - { - "name": "Imin", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": null, - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "Imax", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": null, - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "N", - "base_type": "integer", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": null, - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - }, - { - "name": "status", - "base_type": "logical", - "kind": "", - "rank": 0, - "shape": [], - "lbound": [], - "ubound": [], - "value": null, - "value_type": "unknown", - "is_parameter": false, - "dimensions": [], - "visibility": "public", - "procedure": null, - "intent": "unknown", - "optional": false, - "pass_by_value": false, - "allocatable": false, - "pointer": false - } - ], - "methods": [], - "extends": null, - "attributes": [], - "procedure_bindings": [], - "generic_bindings": [] - } - ], + "derived_types": [], "variables": [], "includes": [], "diagnostics": [],