Skip to content

Commit 1a625e0

Browse files
committed
codex: add pyi readiness context
1 parent 15c69ba commit 1a625e0

7 files changed

Lines changed: 766 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,17 @@ Expected result:
435435
unresolved imported derived-type/kind dependencies, and final `wrappable`
436436
boolean.
437437

438+
If readiness is blocked only because the parsed file imports facts from another
439+
source, an edited `.pyi` file can provide the missing wrapper-facing context:
440+
441+
```bash
442+
python -m x2py solver.f90 --parse --wrap-readiness --readiness-pyi state_mod.pyi
443+
```
444+
445+
The `.pyi` context can declare imported derived types with `class` stubs,
446+
literal compile-time constants with `Final[...] = value`, and callback
447+
signatures with `Callable[...]`.
448+
438449
## Running tests
439450

440451
From repository root:

‎docs/pyi_format.md‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,47 @@ example, `def f(a: Int32) -> None: ...` and
389389
renaming is applied inside shape expressions such as `Shape('1:n')`. Names
390390
outside function and method argument lists, including module variables and class
391391
fields, remain significant.
392+
393+
## Wrap-Readiness Context
394+
395+
An edited `.pyi` file can also be used as parser-side wrap-readiness context
396+
before semantic IR conversion. This is useful when the parsed Fortran source
397+
imports symbols from files that are not part of the current parse, but the user
398+
knows enough about those symbols to make wrapping safe.
399+
400+
Use the CLI with one or more context files:
401+
402+
```bash
403+
python -m x2py solver.f90 --parse --wrap-readiness --readiness-pyi state_mod.pyi
404+
```
405+
406+
The readiness context currently consumes three kinds of facts:
407+
408+
- `class name:` declares an external derived type name.
409+
- `name: Final[Int32] = 8` declares a literal compile-time constant or kind
410+
value.
411+
- `Callable[...]` on a procedure argument declares the callback signature for a
412+
Fortran procedure dummy argument.
413+
414+
Example:
415+
416+
```python
417+
from typing import Callable, Final
418+
419+
rk: Final[Int32] = 8
420+
421+
class sim_state:
422+
n: Int32
423+
values: Float64[Shape('n'), ORDER_F]
424+
425+
def step(
426+
state: sim_state,
427+
t: Float64,
428+
objective: Callable[[sim_state, Float64], Float64],
429+
) -> tuple[Returns["state", sim_state], Returns["score", Float64]]: ...
430+
```
431+
432+
This can clear readiness blockers for a Fortran routine that imports
433+
`sim_state`, uses `real(kind=rk)`, and accepts `objective` as a callback. A
434+
`Final[...]` declaration without a literal value is intentionally not enough
435+
for kind or compile-time value resolution.

‎fortran_parser/cli.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ def _format_blocker_item(code: str, item) -> str:
129129
if code == "unresolved_kind_fields":
130130
providers = ", ".join(item.get("import_modules") or []) or "<not imported>"
131131
return f"{item['type_owner']}:{item['field']} uses kind {item['kind']} from {providers}"
132+
if code == "callback_arguments_requiring_pyi":
133+
iface = f" via {item['interface']}" if item.get("interface") else ""
134+
return f"{item['procedure']}:{item['argument']} needs Callable[...] callback metadata{iface}"
132135
return str(item)
133136

134137

‎fortran_parser/parser.py‎

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from .lexer import preprocess_lines
1010
from .models import FortranArgument, FortranBlockData, FortranDerivedType, FortranFile, FortranInterface, FortranModule, FortranParseError, FortranProcedureSignature, FortranProgram, FortranProject, FortranSubmodule, FortranUseMapping, FortranVariable
11+
from .pyi_context import PyiReadinessContext, load_pyi_readiness_context
1112
from .type_resolver import extract_kind_from_type_spec
1213
from .utils import split_csv
1314

@@ -482,9 +483,17 @@ def visit_project(
482483
self._insert_unique_scope_symbol(project.interfaces, iface.name.lower(), iface, label="project interface scope")
483484
return project
484485

485-
def visit_wrap_readiness(self, code: str, filename: str | None = None) -> dict:
486+
def visit_wrap_readiness(
487+
self,
488+
code: str,
489+
filename: str | None = None,
490+
*,
491+
pyi_files: list[str | Path] | tuple[str | Path, ...] | None = None,
492+
pyi_context: PyiReadinessContext | None = None,
493+
) -> dict:
486494
lines = self._preprocessed_lines(code, filename)
487495
parsed_file = self.visit_file(code, filename=filename)
496+
readiness_context = pyi_context or load_pyi_readiness_context(pyi_files)
488497
modules = parsed_file.modules
489498
submodules = parsed_file.submodules
490499
programs = parsed_file.programs
@@ -525,12 +534,19 @@ def visit_wrap_readiness(self, code: str, filename: str | None = None) -> dict:
525534
wrap_target_signatures,
526535
types,
527536
modules,
537+
pyi_context=readiness_context,
528538
)
529539
unresolved_kind_args, unresolved_kind_fields = self._collect_unresolved_kind_diagnostics(
530540
wrap_target_signatures,
531541
types,
532542
modules,
533543
module_params,
544+
pyi_context=readiness_context,
545+
)
546+
callback_args_requiring_pyi = self._collect_callback_argument_diagnostics(
547+
wrap_target_signatures,
548+
interfaces,
549+
pyi_context=readiness_context,
534550
)
535551
blockers = self._build_wrap_blockers(
536552
signatures=signatures,
@@ -540,6 +556,7 @@ def visit_wrap_readiness(self, code: str, filename: str | None = None) -> dict:
540556
unresolved_derived_fields=unresolved_derived_fields,
541557
unresolved_kind_args=unresolved_kind_args,
542558
unresolved_kind_fields=unresolved_kind_fields,
559+
callback_args_requiring_pyi=callback_args_requiring_pyi,
543560
)
544561
unit_blockers = self._build_unit_blockers(
545562
filename=filename,
@@ -551,6 +568,7 @@ def visit_wrap_readiness(self, code: str, filename: str | None = None) -> dict:
551568
unresolved_derived_fields=unresolved_derived_fields,
552569
unresolved_kind_args=unresolved_kind_args,
553570
unresolved_kind_fields=unresolved_kind_fields,
571+
callback_args_requiring_pyi=callback_args_requiring_pyi,
554572
)
555573

556574
return {
@@ -566,6 +584,8 @@ def visit_wrap_readiness(self, code: str, filename: str | None = None) -> dict:
566584
"unresolved_derived_type_fields": unresolved_derived_fields,
567585
"unresolved_kind_arguments": unresolved_kind_args,
568586
"unresolved_kind_fields": unresolved_kind_fields,
587+
"callback_arguments_requiring_pyi": callback_args_requiring_pyi,
588+
"pyi_context": readiness_context.to_dict(),
569589
"wrappability_blockers": blockers,
570590
"unit_blockers": unit_blockers,
571591
"why_not_wrappable": [b["message"] for b in blockers],
@@ -3717,11 +3737,14 @@ def _kind_symbol_is_known(
37173737
uses: dict[str, list[FortranUseMapping]],
37183738
local_symbols: set[str],
37193739
module_params: dict[str, dict[str, str]],
3740+
pyi_context: PyiReadinessContext | None = None,
37203741
) -> bool:
37213742
"""Check whether a symbolic kind is declared locally or in parsed imports."""
37223743
lowered = symbol.lower()
37233744
if lowered in local_symbols:
37243745
return True
3746+
if pyi_context is not None and pyi_context.has_constant(symbol):
3747+
return True
37253748
if owning_module and lowered in module_params.get(owning_module.lower(), {}):
37263749
return True
37273750
if FortranParser._kind_symbol_visible_from_module_params(symbol, uses, module_params):
@@ -3735,6 +3758,8 @@ def _collect_unresolved_derived_type_diagnostics(
37353758
signatures: list[FortranProcedureSignature],
37363759
types: list[FortranDerivedType],
37373760
modules: list[FortranModule],
3761+
*,
3762+
pyi_context: PyiReadinessContext | None = None,
37383763
) -> tuple[list[dict], list[dict]]:
37393764
"""Find derived-type references that are not defined in the parsed source."""
37403765
defined_types = {dtype.name.lower() for dtype in types}
@@ -3744,7 +3769,9 @@ def _collect_unresolved_derived_type_diagnostics(
37443769

37453770
def _missing_type(kind: str | None) -> bool:
37463771
base_name = FortranParser._derived_type_base_name(kind)
3747-
return bool(base_name) and base_name.lower() not in defined_types
3772+
return bool(base_name) and base_name.lower() not in defined_types and not (
3773+
pyi_context is not None and pyi_context.has_type(base_name)
3774+
)
37483775

37493776
for sig in signatures:
37503777
for arg in sig.arguments:
@@ -3785,6 +3812,8 @@ def _collect_unresolved_kind_diagnostics(
37853812
types: list[FortranDerivedType],
37863813
modules: list[FortranModule],
37873814
module_params: dict[str, dict[str, str]],
3815+
*,
3816+
pyi_context: PyiReadinessContext | None = None,
37883817
) -> tuple[list[dict], list[dict]]:
37893818
"""Find symbolic intrinsic kind references not declared in parsed source/imports."""
37903819
module_uses = {mod.name.lower(): mod.uses for mod in modules}
@@ -3802,6 +3831,7 @@ def _append_unresolved_arg(arg: FortranArgument) -> None:
38023831
uses=sig.uses,
38033832
local_symbols=local_symbols,
38043833
module_params=module_params,
3834+
pyi_context=pyi_context,
38053835
):
38063836
continue
38073837
item = {
@@ -3816,16 +3846,16 @@ def _append_unresolved_arg(arg: FortranArgument) -> None:
38163846
unresolved_args.append(item)
38173847

38183848
for arg in sig.arguments:
3819-
if arg.base_type != "derived":
3849+
if arg.base_type not in {"derived", "procedure"}:
38203850
_append_unresolved_arg(arg)
3821-
if sig.result and sig.result.base_type != "derived":
3851+
if sig.result and sig.result.base_type not in {"derived", "procedure"}:
38223852
_append_unresolved_arg(sig.result)
38233853

38243854
for dtype in types:
38253855
uses = module_uses.get(dtype.module.lower(), {}) if dtype.module else {}
38263856
local_symbols: set[str] = set()
38273857
for field in dtype.fields:
3828-
if field.base_type == "derived":
3858+
if field.base_type in {"derived", "procedure"}:
38293859
continue
38303860
for symbol in sorted(FortranParser._kind_expression_symbols(field.kind)):
38313861
if FortranParser._kind_symbol_is_known(
@@ -3834,6 +3864,7 @@ def _append_unresolved_arg(arg: FortranArgument) -> None:
38343864
uses=uses,
38353865
local_symbols=local_symbols,
38363866
module_params=module_params,
3867+
pyi_context=pyi_context,
38373868
):
38383869
continue
38393870
item = {
@@ -3849,6 +3880,43 @@ def _append_unresolved_arg(arg: FortranArgument) -> None:
38493880

38503881
return unresolved_args, unresolved_fields
38513882

3883+
@staticmethod
3884+
def _collect_callback_argument_diagnostics(
3885+
signatures: list[FortranProcedureSignature],
3886+
interfaces: list[FortranInterface],
3887+
*,
3888+
pyi_context: PyiReadinessContext | None = None,
3889+
) -> list[dict]:
3890+
"""Find procedure dummy arguments whose callback signature is not known."""
3891+
parsed_interfaces = {
3892+
iface.name.lower()
3893+
for iface in interfaces
3894+
if iface.name
3895+
}
3896+
missing_callbacks: list[dict] = []
3897+
3898+
for sig in signatures:
3899+
for arg in sig.arguments:
3900+
if arg.base_type != "procedure":
3901+
continue
3902+
if pyi_context is not None and pyi_context.has_callback_argument(sig.name, arg.name):
3903+
continue
3904+
if arg.kind and arg.kind.lower() in parsed_interfaces:
3905+
continue
3906+
missing_callbacks.append({
3907+
"procedure": sig.name,
3908+
"module": sig.module,
3909+
"argument": arg.name,
3910+
"interface": arg.kind or None,
3911+
"needs": [
3912+
"callable_signature",
3913+
"argument_order",
3914+
"return_type",
3915+
],
3916+
})
3917+
3918+
return missing_callbacks
3919+
38523920
@staticmethod
38533921
def _build_wrap_blockers(
38543922
*,
@@ -3859,6 +3927,7 @@ def _build_wrap_blockers(
38593927
unresolved_derived_fields: list[dict],
38603928
unresolved_kind_args: list[dict],
38613929
unresolved_kind_fields: list[dict],
3930+
callback_args_requiring_pyi: list[dict],
38623931
) -> list[dict]:
38633932
"""Create explicit, user-facing reasons why a source is not wrap-ready."""
38643933
blockers: list[dict] = []
@@ -3904,6 +3973,12 @@ def _build_wrap_blockers(
39043973
"message": "Some derived-type fields use kind symbols missing from the parsed source/imports.",
39053974
"items": unresolved_kind_fields,
39063975
})
3976+
if callback_args_requiring_pyi:
3977+
blockers.append({
3978+
"code": "callback_arguments_requiring_pyi",
3979+
"message": "Some procedure dummy arguments need callback signatures from a .pyi file.",
3980+
"items": callback_args_requiring_pyi,
3981+
})
39073982
return blockers
39083983

39093984
@staticmethod
@@ -3918,6 +3993,7 @@ def _build_unit_blockers(
39183993
unresolved_derived_fields: list[dict],
39193994
unresolved_kind_args: list[dict],
39203995
unresolved_kind_fields: list[dict],
3996+
callback_args_requiring_pyi: list[dict],
39213997
) -> list[dict]:
39223998
"""Build unit-scoped blocker records without per-unit readiness flags.
39233999
@@ -3971,6 +4047,7 @@ def derived_type_unit_key(module: str | None, type_owner: str | None) -> tuple[s
39714047
]
39724048
derived_items = [item for item in unresolved_derived_args if same_unit(item, sig)]
39734049
kind_items = [item for item in unresolved_kind_args if same_unit(item, sig)]
4050+
callback_items = [item for item in callback_args_requiring_pyi if same_unit(item, sig)]
39744051
if missing_items:
39754052
blockers.append({
39764053
"code": "unknown_argument_types",
@@ -3989,6 +4066,12 @@ def derived_type_unit_key(module: str | None, type_owner: str | None) -> tuple[s
39894066
"message": "Some procedure arguments use kind symbols missing from the parsed source/imports.",
39904067
"items": kind_items,
39914068
})
4069+
if callback_items:
4070+
blockers.append({
4071+
"code": "callback_arguments_requiring_pyi",
4072+
"message": "Some procedure dummy arguments need callback signatures from a .pyi file.",
4073+
"items": callback_items,
4074+
})
39924075
if not blockers:
39934076
continue
39944077
qualified_name = f"{sig.module}.{sig.name}" if sig.module else sig.name
@@ -4418,5 +4501,10 @@ def parse_fortran_project(files, *, encoding: str = "utf-8") -> FortranProject:
44184501
return _DEFAULT_PARSER.visit_project(files, encoding=encoding)
44194502

44204503

4421-
def assess_wrap_readiness(code: str, filename: str | None = None) -> dict:
4422-
return _DEFAULT_PARSER.visit_wrap_readiness(code, filename=filename)
4504+
def assess_wrap_readiness(
4505+
code: str,
4506+
filename: str | None = None,
4507+
*,
4508+
pyi_files: list[str | Path] | tuple[str | Path, ...] | None = None,
4509+
) -> dict:
4510+
return _DEFAULT_PARSER.visit_wrap_readiness(code, filename=filename, pyi_files=pyi_files)

0 commit comments

Comments
 (0)