Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 62 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,20 @@ front-end). Current handled coverage:
- Attributes (e.g. `abstract`) and `extends(...)`
- Field extraction (intrinsic + `type(...)`)
- Type-bound procedures and generic bindings
- **Readiness diagnostics**
- Unsupported-pattern detection
- **Parser diagnostics and metadata**
- Source locations for parser errors
- Unknown argument declaration reporting
- Final wrappability summary
- Parse-stage unsupported construct reporting

## Public APIs

Public API:

- `parse_fortran_file(source_or_path, filename=None, macro_defines=None, encoding="utf-8") -> FortranFile`
- `parse_fortran_project(files, encoding="utf-8") -> FortranProject`
- `assess_wrap_readiness(code, filename=None) -> dict`
- `fortran_file_to_semantic_modules(parsed_file, standalone_module_name=None) -> list[SemanticModule]`
- `assess_semantic_wrap_readiness(semantic_ir, source=None) -> dict`
- `assess_pyi_wrap_readiness(path_or_paths, encoding="utf-8") -> dict`

## Repository layout

Expand All @@ -61,11 +63,12 @@ The editable wrapper `.pyi` format is documented in

## Terminal usage

`x2py` exposes three stage flags:
`x2py` exposes four stage flags:

- `--parse` for parser output and parse-stage diagnostics
- `--semantics` for semantic IR JSON
- `--pyi` for generated Python stub text
- `--wrap-readiness` for semantic wrap-readiness from either Fortran or `.pyi`

For parse output, `--show-vars` expands scope-level variables that are normally
summarized as `vars=N`. Use `--print-limit N` to keep large repeated sections
Expand Down Expand Up @@ -223,17 +226,29 @@ Expected JSON structure (top-level keyed by input path):
- `<file>.submodules`: parsed submodules
- `<file>.programs`: parsed programs
- `<file>.block_data`: parsed block data units
- `<file>.wrap_readiness`: readiness diagnostics

### Example 3: wrap-readiness summary

```bash
python -m x2py tests/data/fortran/general/basic_subroutine.f90 --parse --wrap-readiness
python -m x2py tests/data/fortran/general/basic_subroutine.f90 --wrap-readiness
```

This prints the wrappability status and blocker list for each input file.
The JSON readiness payload keeps `wrappable` at file level and includes
`unit_blockers` only for procedure/type/file units that own a blocker.
This converts each input to semantic IR, then prints the wrappability status
and blocker list. The same flag accepts edited `.pyi` files:

```bash
python -m x2py solver.pyi --wrap-readiness
```

Use `--json` for the stable readiness payload. It keeps `wrappable` at file
level and includes `unit_blockers` only for units that own a blocker.

`--wrap-readiness` can also be combined with other stages. For example,
`--semantics --wrap-readiness` emits semantic IR with a `wrap_readiness` payload
attached, and `--parse --wrap-readiness` prints the parse tree followed by the
semantic readiness summary. Parser JSON remains parse-only; when `--json` is
used with `--parse --wrap-readiness`, the output is split into top-level
`parse` and `wrap_readiness` sections.

### Example 4: semantic IR JSON output

Expand Down Expand Up @@ -415,25 +430,41 @@ Expected result:

```python
from pathlib import Path
from x2py import parse_fortran_file, assess_wrap_readiness
from x2py import (
assess_semantic_wrap_readiness,
fortran_file_to_semantic_modules,
parse_fortran_file,
)

path = Path("tests/data/fortran/general/basic_subroutine.f90")
code = path.read_text()

parsed = parse_fortran_file(code, filename=str(path))
report = assess_wrap_readiness(code, filename=str(path))
modules = fortran_file_to_semantic_modules(parsed, standalone_module_name=path.stem)
report = assess_semantic_wrap_readiness(modules, source=str(path))

print("procedures:", len(parsed.procedures))
print("wrappable:", report["wrappable"])
print("unknown args:", report["unknown_argument_types"])
print("blockers:", report["why_not_wrappable"])
```

Expected result:

- `parsed` is a `FortranFile` aggregate with procedures/modules/types/interfaces/program units.
- `report` includes counts, unsupported construct hits, unknown argument info,
unresolved imported derived-type/kind dependencies, and final `wrappable`
boolean.
- `report` is produced from semantic IR and includes public API counts,
semantic blockers, unit-level blockers, and final `wrappable` boolean.

If the parsed Fortran file cannot describe the wrapper interface completely,
generate a draft `.pyi`, edit it, then assess readiness from the edited stub:

```bash
python -m x2py solver.f90 --pyi --out solver.pyi
python -m x2py solver.pyi --wrap-readiness
```

The edited `.pyi` is the source of truth for readiness. It can declare derived
types with `class` stubs, literal compile-time constants with
`Final[...] = value`, and callback signatures with `Callable[[...], ...]`.

## Running tests

Expand Down Expand Up @@ -479,13 +510,27 @@ short explanation in the PR. For `.pyi` or semantic IR behavior changes, update
the corresponding fixtures under `tests/pyi/fixtures` or
`tests/semantics/fixtures`.

Semantic wrap-readiness corpus messages for the general, BLAS, LAPACK, and
SciFortran fixtures are regenerated separately:

```bash
python tests/semantics/generate_wrap_readiness_fixtures.py
```

This writes `tests/semantics/fixtures/wrap_readiness_messages.json`. The file is
a semantic readiness fixture, not a parser golden, even though Fortran fixtures
are used as input.

## Semantic parser structure

The parser exposes stable file/project entrypoints:

- `parse_fortran_file(...)` for one source (string or path) returning `FortranFile`.
- `parse_fortran_project(...)` for many sources returning `FortranProject`.
- `assess_wrap_readiness(...)` for wrappability diagnostics.

Wrap-readiness is intentionally outside the parser model. Use
`fortran_file_to_semantic_modules(...)` or `.pyi` parsing to produce semantic IR,
then call `assess_semantic_wrap_readiness(...)` on that semantic interface.

Internally, `FortranParser.visit_file` uses a recursive grammar-style
source-unit parser. The file is first sliced into direct
Expand Down
53 changes: 53 additions & 0 deletions docs/pyi_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,56 @@ example, `def f(a: Int32) -> None: ...` and
renaming is applied inside shape expressions such as `Shape('1:n')`. Names
outside function and method argument lists, including module variables and class
fields, remain significant.

## Semantic Wrap-Readiness

Readiness is assessed from semantic IR, not from parser internals. The same CLI
flag works for either source path:

```bash
python -m x2py solver.f90 --wrap-readiness
python -m x2py solver.pyi --wrap-readiness
```

For Fortran input, x2py parses the source, converts it to semantic IR, then
checks that semantic interface. For `.pyi` input, x2py parses the edited stub
directly to semantic IR and checks that interface. The edited `.pyi` is the
source of truth when the user needs to provide information the source parser
cannot infer.

The flag can be requested alone for a concise readiness report or combined with
other stages. For example, `--semantics --wrap-readiness` emits semantic IR with
the readiness payload attached.

The readiness check currently consumes these `.pyi` facts:

- `class name:` declares a wrapper-visible derived type or handle.
- `name: Final[Int32] = 8` declares a literal compile-time constant value that
can satisfy shape and size metadata.
- `Callable[[ArgType, ...], ReturnType]` declares the full callback signature
for a procedure/function-pointer argument.

Example:

```python
from typing import Callable, Final

rk: Final[Int32] = 8

class sim_state:
n: Int32
values: Float64[Shape('n'), ORDER_F]

def step(
state: sim_state,
t: Float64,
objective: Callable[[sim_state, Float64], Float64],
) -> tuple[Returns["state", sim_state], Returns["score", Float64]]: ...
```

This can clear readiness blockers for a Fortran routine that imports
`sim_state`, uses `real(kind=rk)`, and accepts `objective` as a callback. A
`Final[...]` declaration without a literal value is intentionally not enough
for compile-time shape or size resolution, and `Callable[..., ReturnType]` is
not enough for callbacks because the wrapper still needs argument order and
argument types.
60 changes: 0 additions & 60 deletions fortran_parser/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ def _parse_paths(paths: list[str]) -> dict[str, dict]:
"submodules": [_to_dict_no_parent(m) for m in parsed.submodules],
"programs": [_to_dict_no_parent(m) for m in parsed.programs],
"block_data": [_to_dict_no_parent(m) for m in parsed.block_data_units],
"wrap_readiness": parser.visit_wrap_readiness(code, filename=str(p)),
}
return out

Expand Down Expand Up @@ -111,50 +110,6 @@ def _format_pyi_report(semantic_report: dict[str, dict]) -> str:
lines.append("")
return "\n".join(lines).rstrip()

def _format_blocker_item(code: str, item) -> str:
"""Format one wrap-readiness blocker item for human-readable output."""
if code == "unsupported_constructs":
return f"line {item['line']}: {item['text']}"
if code == "unknown_argument_types":
return str(item)
if code == "unresolved_derived_type_arguments":
providers = ", ".join(item.get("import_modules") or []) or "<not imported>"
return f"{item['procedure']}:{item['argument']} uses type({item['type']}) from {providers}"
if code == "unresolved_derived_type_fields":
providers = ", ".join(item.get("import_modules") or []) or "<not imported>"
return f"{item['type_owner']}:{item['field']} uses type({item['type']}) from {providers}"
if code == "unresolved_kind_arguments":
providers = ", ".join(item.get("import_modules") or []) or "<not imported>"
return f"{item['procedure']}:{item['argument']} uses kind {item['kind']} from {providers}"
if code == "unresolved_kind_fields":
providers = ", ".join(item.get("import_modules") or []) or "<not imported>"
return f"{item['type_owner']}:{item['field']} uses kind {item['kind']} from {providers}"
return str(item)


def _format_wrap_readiness(report: dict[str, dict]) -> str:
"""Format only wrap-readiness status and blockers for each parsed file."""
lines: list[str] = []
for fname, parsed in report.items():
readiness = parsed["wrap_readiness"]
status = "yes" if readiness["wrappable"] else "no"
lines.append(f"File: {fname}")
lines.append(f" Wrappable: {status}")
blockers = readiness.get("wrappability_blockers", [])
if blockers:
lines.append(" Why not wrappable:")
for blocker in blockers:
lines.append(f" - {blocker['message']}")
for item in blocker.get("items", []):
lines.append(f" * {_format_blocker_item(blocker['code'], item)}")
else:
lines.append(" No wrap-readiness blockers detected.")
lines.append("")
return "\n".join(lines).rstrip()




def _format_var_type(var: dict) -> str:
base = var.get("base_type", "unknown")
kind = var.get("kind")
Expand Down Expand Up @@ -294,14 +249,6 @@ def _format_report(
if hidden_blocks > 0:
lines.append(f" ... {hidden_blocks} more block data units")

# readiness = parsed["wrap_readiness"]
# lines.append(f" Wrappable: {'yes' if readiness['wrappable'] else 'no'}")
# if readiness.get("wrappability_blockers"):
# lines.append(" Why not wrappable:")
# for blocker in readiness["wrappability_blockers"]:
# lines.append(f" - {blocker['message']}")
# for item in blocker.get("items", []):
# lines.append(f" * {_format_blocker_item(blocker['code'], item)}")
lines.append("")
return "\n".join(lines).rstrip()

Expand All @@ -319,11 +266,6 @@ def main() -> int:
parser.add_argument("--json", action="store_true", help="Print JSON to stdout")
parser.add_argument("--semantics", action="store_true", help="Generate semantic IR models from parsed Fortran modules")
parser.add_argument("--pyi", action="store_true", help="Print the generated Python .pyi content from semantic models")
parser.add_argument(
"--wrap-readiness",
action="store_true",
help="Print only whether each input is wrap-ready and, when it is not, why.",
)
parser.add_argument(
"--show-vars",
action="store_true",
Expand Down Expand Up @@ -379,8 +321,6 @@ def main() -> int:
print(json.dumps(payload, indent=2))
elif args.pyi:
print(_format_pyi_report(semantic or {}))
elif args.wrap_readiness:
print(_format_wrap_readiness(report))
elif args.semantics:
print(json.dumps(payload, indent=2))
else:
Expand Down
Loading
Loading