nativegate 0.1.7__tar.gz → 0.1.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nativegate-0.1.7 → nativegate-0.1.9}/PKG-INFO +8 -1
- {nativegate-0.1.7 → nativegate-0.1.9}/README.md +7 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/fortran.py +42 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/fortran_fparser.py +59 -6
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/fortran_regex.py +75 -5
- nativegate-0.1.9/nativegate/parsers/hollerith.py +93 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate.egg-info/PKG-INFO +8 -1
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate.egg-info/SOURCES.txt +3 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/pyproject.toml +1 -1
- nativegate-0.1.9/tests/test_external_callbacks.py +79 -0
- nativegate-0.1.9/tests/test_hollerith.py +84 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/__init__.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/__main__.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/buildinfo.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/cli.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/config.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/declared_invariants.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/discovery.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/driverbuild.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/drivers/__init__.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/drivers/cpp.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/drivers/fortran.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/__init__.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/cmake_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/docker_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/error_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/f2py_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/gateway_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/golden_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/k8s_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/mcp_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/middleware_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/pybind_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/pyproject_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/python_pkg_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/generators/test_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/golden.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/invariants.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/ir.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/lattice.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/locking.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/oracle.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/__init__.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/cpp.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/cpp_ast.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/cpp_regex.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/dialect.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/parsers/fixed_form.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/preprocess.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/structural_invariants.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/suggest.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/templates/__init__.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/templates/golden_test_template.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate/wire.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate.egg-info/dependency_links.txt +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate.egg-info/entry_points.txt +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate.egg-info/requires.txt +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/nativegate.egg-info/top_level.txt +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/setup.cfg +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_buildinfo.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cli_serve_verify.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cli_suggest.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cli_verify_aggregate.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cmake_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cmake_missing_input.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_config_unknown_keys.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cpp_ast.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cpp_driver.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_cpp_pipeline.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_declared_invariants.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_derived_types.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_dialect.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_docker_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_driverbuild.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_endpoint_docstrings.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_fixed_form.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_fortran_doc_comments.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_fortran_driver.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_fortran_fparser.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_fortran_libraries.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_fortran_pipeline.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_gateway_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_generated_python_is_valid.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_golden.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_inspect_dialect.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_invariants.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_ir_contract.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_k8s_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_lattice.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_legacy_cpp.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_legacy_fortran.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_locking.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_mcp_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_middleware_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_multifile_cpp.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_native_oracle.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_native_state_contract.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_oracle.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_preprocess_and_config.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_preprocessed_fortran.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_pybind_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_pyproject_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_quickstart.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_scalar_ref.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_service_gen.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_service_name_validation.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_shared_libraries.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_structural_invariants.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_suggest.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_verification_config.py +0 -0
- {nativegate-0.1.7 → nativegate-0.1.9}/tests/test_wire.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nativegate
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -552,3 +552,10 @@ mapping. Each refusal is printed at `generate` time with its reason, and the
|
|
|
552
552
|
generated service publishes them at `GET /_unexposed` so they stay visible to
|
|
553
553
|
whoever is calling the API. An empty mapping there means this build refused
|
|
554
554
|
nothing; a 404 means the service predates the route and cannot tell you.
|
|
555
|
+
|
|
556
|
+
## Community
|
|
557
|
+
|
|
558
|
+
Contributing: see the repository's [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
559
|
+
Participation in this project's spaces is covered by the
|
|
560
|
+
[Code of Conduct](../../CODE_OF_CONDUCT.md); reports go to
|
|
561
|
+
sr.rabiu@gmail.com.
|
|
@@ -519,3 +519,10 @@ mapping. Each refusal is printed at `generate` time with its reason, and the
|
|
|
519
519
|
generated service publishes them at `GET /_unexposed` so they stay visible to
|
|
520
520
|
whoever is calling the API. An empty mapping there means this build refused
|
|
521
521
|
nothing; a 404 means the service predates the route and cannot tell you.
|
|
522
|
+
|
|
523
|
+
## Community
|
|
524
|
+
|
|
525
|
+
Contributing: see the repository's [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
526
|
+
Participation in this project's spaces is covered by the
|
|
527
|
+
[Code of Conduct](../../CODE_OF_CONDUCT.md); reports go to
|
|
528
|
+
sr.rabiu@gmail.com.
|
|
@@ -65,10 +65,12 @@ from __future__ import annotations
|
|
|
65
65
|
|
|
66
66
|
import os
|
|
67
67
|
from pathlib import Path
|
|
68
|
+
import tempfile
|
|
68
69
|
|
|
69
70
|
from ..config import ExposeConfig
|
|
70
71
|
from ..ir import ModuleIR
|
|
71
72
|
from . import fortran_fparser, fortran_regex
|
|
73
|
+
from . import hollerith
|
|
72
74
|
|
|
73
75
|
# Re-exported so callers that reach for the free-form continuation joiner (the
|
|
74
76
|
# CLI's source-rewriting paths, and a good number of tests) keep working after
|
|
@@ -137,6 +139,31 @@ def backend_description(backend: str | None = None) -> str:
|
|
|
137
139
|
)
|
|
138
140
|
|
|
139
141
|
|
|
142
|
+
def _hollerith_resolved(path: Path) -> Path | None:
|
|
143
|
+
"""A temp copy of `path` with Hollerith constants resolved, or None.
|
|
144
|
+
|
|
145
|
+
fparser2 refuses Hollerith constants gfortran compiles with a warning —
|
|
146
|
+
the one measured counterexample to the parity rule that made fparser2
|
|
147
|
+
the default (DEFECTS D15, netlib quadpack). The file gfortran accepts
|
|
148
|
+
is the file that should parse, so the front door resolves the constants
|
|
149
|
+
into their standard quoted spelling and reads that copy instead. The
|
|
150
|
+
copy is attributed back to the original path, so source-file references
|
|
151
|
+
and doc-comment line spans keep pointing at the real file.
|
|
152
|
+
"""
|
|
153
|
+
raw = path.read_text()
|
|
154
|
+
if not hollerith.is_hollerith(raw):
|
|
155
|
+
return None
|
|
156
|
+
resolved, _ = hollerith.resolve_hollerith(raw)
|
|
157
|
+
handle, name = tempfile.mkstemp(suffix=path.suffix)
|
|
158
|
+
Path(name).write_text(resolved)
|
|
159
|
+
# mkstemp exposes an int fd; close it — Path.write_text already used
|
|
160
|
+
# a second open, and the raw descriptor must not leak.
|
|
161
|
+
import os
|
|
162
|
+
|
|
163
|
+
os.close(handle)
|
|
164
|
+
return Path(name)
|
|
165
|
+
|
|
166
|
+
|
|
140
167
|
def parse_source(
|
|
141
168
|
path: Path,
|
|
142
169
|
expose: ExposeConfig,
|
|
@@ -146,6 +173,15 @@ def parse_source(
|
|
|
146
173
|
) -> ModuleIR:
|
|
147
174
|
"""Parse one Fortran source into a ModuleIR using the selected backend."""
|
|
148
175
|
if resolve_backend(backend) == "fparser2":
|
|
176
|
+
resolved = _hollerith_resolved(path)
|
|
177
|
+
if resolved is not None:
|
|
178
|
+
module = fortran_fparser.parse_source(resolved, expose, include_paths)
|
|
179
|
+
# The temp copy must not leak into user-visible IR: same name,
|
|
180
|
+
# same source file, only the text differs (D15).
|
|
181
|
+
module.name = path.stem
|
|
182
|
+
module.source_file = str(path)
|
|
183
|
+
resolved.unlink(missing_ok=True)
|
|
184
|
+
return module
|
|
149
185
|
return fortran_fparser.parse_source(path, expose, include_paths)
|
|
150
186
|
return fortran_regex.parse_source(path, expose, include_paths)
|
|
151
187
|
|
|
@@ -153,5 +189,11 @@ def parse_source(
|
|
|
153
189
|
def list_routine_names(path: Path, *, backend: str | None = None) -> list[str]:
|
|
154
190
|
"""Every function/subroutine name in the file, in source order."""
|
|
155
191
|
if resolve_backend(backend) == "fparser2":
|
|
192
|
+
resolved = _hollerith_resolved(path)
|
|
193
|
+
if resolved is not None:
|
|
194
|
+
try:
|
|
195
|
+
return fortran_fparser.list_routine_names(resolved)
|
|
196
|
+
finally:
|
|
197
|
+
resolved.unlink(missing_ok=True)
|
|
156
198
|
return fortran_fparser.list_routine_names(path)
|
|
157
199
|
return fortran_regex.list_routine_names(path)
|
|
@@ -454,6 +454,13 @@ class _Declarations:
|
|
|
454
454
|
derived_intents: dict[str, str] = field(default_factory=dict)
|
|
455
455
|
# Names given a shape by a standalone DIMENSION statement.
|
|
456
456
|
dimensioned: set[str] = field(default_factory=set)
|
|
457
|
+
# Names declared EXTERNAL — Fortran procedure dummies (callbacks). f2py
|
|
458
|
+
# binds these only with per-argument callback directives, which nativegate
|
|
459
|
+
# does not generate; typing one by its IMPLICIT fallback (`real`, because
|
|
460
|
+
# 'f' is not in the I-N band) produced a binding that builds, imports,
|
|
461
|
+
# passes its smoke test and hands Fortran garbage at call time. Recorded
|
|
462
|
+
# first contact: netlib quadpack's QNG/QAGSE (DEFECTS D14).
|
|
463
|
+
external: set[str] = field(default_factory=set)
|
|
457
464
|
|
|
458
465
|
|
|
459
466
|
def _parse_declarations(spec_part) -> _Declarations:
|
|
@@ -468,6 +475,17 @@ def _parse_declarations(spec_part) -> _Declarations:
|
|
|
468
475
|
for item in stmt.items[0]:
|
|
469
476
|
result.dimensioned.add(_name_of(item[0]).lower())
|
|
470
477
|
|
|
478
|
+
# EXTERNAL names are Fortran procedure dummies — callbacks. See the
|
|
479
|
+
# `external` field note above for why typing them is a silently wrong
|
|
480
|
+
# answer, and DEFECTS D14 for the netlib quadpack contact. External_Name
|
|
481
|
+
# nodes never appear as standalone tree entries, so read the names off
|
|
482
|
+
# External_Stmt.items: item 0 is the keyword, the last is the name list
|
|
483
|
+
# (the arity lines up across the fixed- and free-form grammars).
|
|
484
|
+
for stmt in _walk(spec_part, _F.External_Stmt):
|
|
485
|
+
name_list = stmt.items[-1]
|
|
486
|
+
for item in getattr(name_list, "items", ()):
|
|
487
|
+
result.external.add(_name_of(item).lower())
|
|
488
|
+
|
|
471
489
|
for decl in _walk(spec_part, _F.Type_Declaration_Stmt):
|
|
472
490
|
type_spec = decl.items[0]
|
|
473
491
|
if _is_derived(type_spec):
|
|
@@ -698,17 +716,30 @@ def _resolve_parameters(
|
|
|
698
716
|
declared: dict[str, Parameter],
|
|
699
717
|
unbindable: dict[str, str],
|
|
700
718
|
implicit_map: dict[str, str],
|
|
719
|
+
external_names: set[str] = frozenset(),
|
|
701
720
|
) -> list[Parameter]:
|
|
702
721
|
"""Type every dummy argument, or refuse the routine.
|
|
703
722
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
723
|
+
Four sources in decreasing authority: EXTERNAL declarations (refuse the
|
|
724
|
+
routine — callbacks have their own semantic and no generated binding),
|
|
725
|
+
an explicit declaration, a recognised-but-unbindable declaration
|
|
726
|
+
(refuse), and IMPLICIT typing. There is deliberately no fifth "assume
|
|
727
|
+
float" step — the reason the reasons below exist at all.
|
|
708
728
|
"""
|
|
709
729
|
parameters: list[Parameter] = []
|
|
710
730
|
for param_name in param_names:
|
|
711
731
|
key = param_name.lower()
|
|
732
|
+
# EXTERNAL check ahead of every type lookup: a name in the external
|
|
733
|
+
# list is a procedure dummy, whatever it implicitly types as. 'f' is
|
|
734
|
+
# not in the I-N band, so the fallback it used to hit was real/float.
|
|
735
|
+
if key in external_names:
|
|
736
|
+
raise _RoutineSkipped(
|
|
737
|
+
f"argument '{param_name}' is declared EXTERNAL (a Fortran "
|
|
738
|
+
"procedure/callback). f2py binds callbacks only through per-"
|
|
739
|
+
"argument f2py directives, which the generator does not emit "
|
|
740
|
+
"yet — this routine is skipped as a whole rather than bound "
|
|
741
|
+
"with the callback mis-typed as a scalar."
|
|
742
|
+
)
|
|
712
743
|
if key in declared:
|
|
713
744
|
parameters.append(declared[key])
|
|
714
745
|
continue
|
|
@@ -824,11 +855,11 @@ def _free_form_routine(
|
|
|
824
855
|
parameters.extend(flat_by_param[param])
|
|
825
856
|
else:
|
|
826
857
|
parameters.extend(
|
|
827
|
-
_resolve_parameters([param], declared, unbindable, implicit_map)
|
|
858
|
+
_resolve_parameters([param], declared, unbindable, implicit_map, decls.external)
|
|
828
859
|
)
|
|
829
860
|
else:
|
|
830
861
|
parameters = _resolve_parameters(
|
|
831
|
-
param_names, declared, unbindable, implicit_map
|
|
862
|
+
param_names, declared, unbindable, implicit_map, decls.external
|
|
832
863
|
)
|
|
833
864
|
|
|
834
865
|
if is_subroutine:
|
|
@@ -987,8 +1018,26 @@ def _parse_fixed_form(path: Path, expose: ExposeConfig, include_paths: list[Path
|
|
|
987
1018
|
)
|
|
988
1019
|
|
|
989
1020
|
parameters = []
|
|
1021
|
+
external_refused = False
|
|
990
1022
|
for param_name in param_names:
|
|
991
1023
|
key = param_name.lower()
|
|
1024
|
+
# Ahead of every type lookup: an EXTERNAL name is a procedure
|
|
1025
|
+
# dummy whatever it implicitly types as (DEFECTS D14 — netlib
|
|
1026
|
+
# quadpack QNG/QAGSE).
|
|
1027
|
+
if key in decls.external:
|
|
1028
|
+
module.skipped.append(
|
|
1029
|
+
SkippedSymbol(
|
|
1030
|
+
name,
|
|
1031
|
+
f"argument '{param_name}' is declared EXTERNAL (a "
|
|
1032
|
+
"Fortran procedure/callback). f2py binds callbacks "
|
|
1033
|
+
"only through per-argument f2py directives, which "
|
|
1034
|
+
"the generator does not emit yet — skipped as a "
|
|
1035
|
+
"whole rather than bound with the callback "
|
|
1036
|
+
"mis-typed as a scalar.",
|
|
1037
|
+
)
|
|
1038
|
+
)
|
|
1039
|
+
external_refused = True
|
|
1040
|
+
break
|
|
992
1041
|
declaration = decls.declared.get(key)
|
|
993
1042
|
if declaration is not None:
|
|
994
1043
|
py_type = declaration.type
|
|
@@ -1068,6 +1117,10 @@ def _parse_fixed_form(path: Path, expose: ExposeConfig, include_paths: list[Path
|
|
|
1068
1117
|
)
|
|
1069
1118
|
returns = map_fortran_type(base) if base else "float"
|
|
1070
1119
|
|
|
1120
|
+
if external_refused:
|
|
1121
|
+
# Recorded in module.skipped; nothing downstream may wrap it.
|
|
1122
|
+
continue
|
|
1123
|
+
|
|
1071
1124
|
module.functions.append(
|
|
1072
1125
|
FunctionDef(
|
|
1073
1126
|
name=name,
|
|
@@ -299,6 +299,29 @@ def _parse_derived_declarations(body: str) -> dict[str, str]:
|
|
|
299
299
|
return unbindable
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
# EXTERNAL declarations name Fortran procedure dummies — callbacks. Stands
|
|
303
|
+
# in the fixed-form `external f` shape AND the free-form `external :: f, g`
|
|
304
|
+
# one. Collected so the routine that takes one is skipped with a reason
|
|
305
|
+
# (both readers, identical text): typing it through the IMPLICIT fallback —
|
|
306
|
+
# 'f' is real, since 'f' sits outside the I-N integer band — produced a
|
|
307
|
+
# binding that builds, imports, smokes green and hands Fortran garbage at
|
|
308
|
+
# call time. First contact: netlib quadpack QNG/QAGSE (DEFECTS D14).
|
|
309
|
+
_EXTERNAL_RE = re.compile(
|
|
310
|
+
r"^[ \t]*external[ \t]*(?::[ \t]*)?([a-z][a-z0-9_, \t]*)$",
|
|
311
|
+
re.IGNORECASE | re.MULTILINE,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _external_declared_names(body: str) -> set[str]:
|
|
316
|
+
out: set[str] = set()
|
|
317
|
+
for match in _EXTERNAL_RE.finditer(body):
|
|
318
|
+
for raw in match.group(1).split(","):
|
|
319
|
+
name = raw.strip().lower()
|
|
320
|
+
if name:
|
|
321
|
+
out.add(name)
|
|
322
|
+
return out
|
|
323
|
+
|
|
324
|
+
|
|
302
325
|
def _parse_declarations(body: str) -> dict[str, Parameter]:
|
|
303
326
|
"""Map declared variable name -> Parameter, for every `type, attrs :: names` line in body."""
|
|
304
327
|
declared: dict[str, Parameter] = {}
|
|
@@ -336,19 +359,34 @@ def _resolve_parameters(
|
|
|
336
359
|
declared: dict[str, Parameter],
|
|
337
360
|
unbindable: dict[str, str],
|
|
338
361
|
implicit_map: dict[str, str],
|
|
362
|
+
external_names=None,
|
|
339
363
|
) -> list[Parameter]:
|
|
340
364
|
"""Type every dummy argument, or refuse the routine.
|
|
341
365
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
366
|
+
Four sources in decreasing authority: EXTERNAL declarations (refuse the
|
|
367
|
+
routine — a callback typed via the IMPLICIT fallback is a buildable,
|
|
368
|
+
importable, silently wrong binding; see DEFECTS D14 / netlib quadpack),
|
|
369
|
+
an explicit declaration, a recognised-but-unbindable declaration
|
|
370
|
+
(refuse), and IMPLICIT typing. There is deliberately no fifth "assume
|
|
371
|
+
float" step.
|
|
345
372
|
"""
|
|
373
|
+
external_names = external_names if external_names is not None else set()
|
|
346
374
|
raw_params = match.group("params") or ""
|
|
347
375
|
param_names = [p.strip() for p in raw_params.split(",") if p.strip()]
|
|
348
376
|
|
|
349
377
|
parameters: list[Parameter] = []
|
|
350
378
|
for param_name in param_names:
|
|
351
379
|
key = param_name.lower()
|
|
380
|
+
# Ahead of every type lookup: an EXTERNAL name is a procedure dummy
|
|
381
|
+
# whatever it implicitly types as — see the note on _EXTERNAL_RE.
|
|
382
|
+
if key in (external_names or ()):
|
|
383
|
+
raise _RoutineSkipped(
|
|
384
|
+
f"argument '{param_name}' is declared EXTERNAL (a Fortran "
|
|
385
|
+
"procedure/callback). f2py binds callbacks only through per-"
|
|
386
|
+
"argument f2py directives, which the generator does not emit "
|
|
387
|
+
"yet — this routine is skipped as a whole rather than bound "
|
|
388
|
+
"with the callback mis-typed as a scalar."
|
|
389
|
+
)
|
|
352
390
|
if key in declared:
|
|
353
391
|
parameters.append(declared[key])
|
|
354
392
|
continue
|
|
@@ -431,7 +469,9 @@ def _extract_function(
|
|
|
431
469
|
declared = _parse_declarations(body)
|
|
432
470
|
unbindable = _parse_derived_declarations(body)
|
|
433
471
|
|
|
434
|
-
parameters = _resolve_parameters(
|
|
472
|
+
parameters = _resolve_parameters(
|
|
473
|
+
match, declared, unbindable, implicit_map, _external_declared_names(body)
|
|
474
|
+
)
|
|
435
475
|
|
|
436
476
|
result_name = (match.group("result") or name).lower()
|
|
437
477
|
result_param = declared.get(result_name)
|
|
@@ -476,7 +516,9 @@ def _extract_subroutine(
|
|
|
476
516
|
declared = _parse_declarations(body)
|
|
477
517
|
unbindable = _parse_derived_declarations(body)
|
|
478
518
|
|
|
479
|
-
parameters = _resolve_parameters(
|
|
519
|
+
parameters = _resolve_parameters(
|
|
520
|
+
match, declared, unbindable, implicit_map, _external_declared_names(body)
|
|
521
|
+
)
|
|
480
522
|
|
|
481
523
|
enclosing = _enclosing_fortran_module(source, match.start())
|
|
482
524
|
fn = FunctionDef(
|
|
@@ -537,9 +579,32 @@ def _parse_fixed_form_source(
|
|
|
537
579
|
# `STEP(DTIN, DTOUT, ICONV)` would return None.
|
|
538
580
|
intents = fixed_form.infer_intents(routine, normalized)
|
|
539
581
|
|
|
582
|
+
# The fixed-form half has its own declaration scan (`find_routine` +
|
|
583
|
+
# `declared_types`), which never consulted the free-form extractors
|
|
584
|
+
# — so an `external f` (a Fortran procedure dummy) typed straight
|
|
585
|
+
# through the IMPLICIT fallback to `real`/float and produced a
|
|
586
|
+
# binding that builds, imports, passes its smoke test and hands
|
|
587
|
+
# Fortran garbage at call time. Found on netlib quadpack QNG/QAGSE
|
|
588
|
+
# (DEFECTS D14); refusal is the interpretation both backends share.
|
|
589
|
+
external_names = _external_declared_names(routine["body"])
|
|
540
590
|
parameters = []
|
|
541
591
|
for param_name in routine["params"]:
|
|
542
592
|
key = param_name.lower()
|
|
593
|
+
# Ahead of every type lookup: an EXTERNAL name is a procedure
|
|
594
|
+
# dummy whatever it implicitly types as — see the note above.
|
|
595
|
+
if key in external_names:
|
|
596
|
+
module.skipped.append(
|
|
597
|
+
SkippedSymbol(
|
|
598
|
+
name,
|
|
599
|
+
f"argument '{param_name}' is declared EXTERNAL (a "
|
|
600
|
+
"Fortran procedure/callback). f2py binds callbacks "
|
|
601
|
+
"only through per-argument f2py directives, which "
|
|
602
|
+
"the generator does not emit yet — skipped as a "
|
|
603
|
+
"whole rather than bound with the callback "
|
|
604
|
+
"mis-typed as a scalar.",
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
break
|
|
543
608
|
base_type = routine["declared_types"].get(key) or fixed_form.implicit_type_of(
|
|
544
609
|
param_name, implicit_map
|
|
545
610
|
)
|
|
@@ -602,6 +667,11 @@ def _parse_fixed_form_source(
|
|
|
602
667
|
)
|
|
603
668
|
returns = map_fortran_type(base) if base else "float"
|
|
604
669
|
|
|
670
|
+
# A routine whose whole binding was refused (external callback) has
|
|
671
|
+
# been recorded in module.skipped; nothing downstream may wrap it.
|
|
672
|
+
if any(s.name == name for s in module.skipped):
|
|
673
|
+
continue
|
|
674
|
+
|
|
605
675
|
module.functions.append(
|
|
606
676
|
FunctionDef(
|
|
607
677
|
name=name,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Hollerith constants in fixed-form Fortran — rewrite to CHARACTER literals.
|
|
2
|
+
|
|
3
|
+
The netlib quadpack contact (DEFECTS D15) is the canonical case: 1970s-era
|
|
4
|
+
sources pass error text as Hollerith constants,
|
|
5
|
+
|
|
6
|
+
call xerror(26habnormal return from qng ,26,ier,0)
|
|
7
|
+
|
|
8
|
+
gfortran compiles that (legacy extension, warning only), but fparser2
|
|
9
|
+
refuses the whole file — the first measured counterexample to the
|
|
10
|
+
"fparser2 is never stricter than gfortran" rule that justifies fparser2
|
|
11
|
+
being the default front end. Since nativegate must compile with gfortran
|
|
12
|
+
anyway, the file gfortran accepts is exactly the file that should reach
|
|
13
|
+
the parser; the quoted CHARACTER constant is the standard F77 spelling of
|
|
14
|
+
the same argument, which f2py's own reader accepts too.
|
|
15
|
+
|
|
16
|
+
Deliberately conservative:
|
|
17
|
+
|
|
18
|
+
* Fixed-form text only; F90+ sources do not carry Hollerith.
|
|
19
|
+
* One constant per physical line: an `N*H` whose count runs past the line
|
|
20
|
+
end (it continues onto the next) is left untouched — rewriting it would
|
|
21
|
+
move every following line number and shift the doc-comment spans both
|
|
22
|
+
backends position by line.
|
|
23
|
+
* The rewritten line must fit the punch-card margin (72 columns). One
|
|
24
|
+
that cannot is left alone. What survives un-resolved is the caller's
|
|
25
|
+
decision (see resolver_hollerith in parsers/fortran.py).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
_HOLLERITH_START_RE = re.compile(r"(?<!\w)(\d+)\s*[hH]", re.ASCII)
|
|
33
|
+
|
|
34
|
+
_FIXED_FORM_LINE_LIMIT = 72
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_hollerith(text: str) -> bool:
|
|
38
|
+
return bool(_HOLLERITH_START_RE.search(text))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_hollerith(text: str) -> tuple[str, int]:
|
|
42
|
+
"""Replace resolvable Hollerith constants with quoted literals, in line.
|
|
43
|
+
|
|
44
|
+
Returns (text, resolved). Line count is always preserved, so line-based
|
|
45
|
+
spans (doc comments, fparser2 item spans) stay put — the same invariant
|
|
46
|
+
the dialect pass keeps.
|
|
47
|
+
"""
|
|
48
|
+
if not is_hollerith(text):
|
|
49
|
+
return text, 0
|
|
50
|
+
|
|
51
|
+
out_lines = []
|
|
52
|
+
resolved_total = 0
|
|
53
|
+
for line in text.splitlines(keepends=True):
|
|
54
|
+
resolved_line, n = _resolve_line(line)
|
|
55
|
+
out_lines.append(resolved_line)
|
|
56
|
+
resolved_total += n
|
|
57
|
+
return "".join(out_lines), resolved_total
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _resolve_line(line: str) -> tuple[str, int]:
|
|
61
|
+
"""Substitute every fitting Hollerith constant on one physical line."""
|
|
62
|
+
end_of_line = len(line.rstrip("\r\n"))
|
|
63
|
+
eol = line[end_of_line:]
|
|
64
|
+
body = line[:end_of_line]
|
|
65
|
+
|
|
66
|
+
resolved = 0
|
|
67
|
+
position = 0
|
|
68
|
+
while position <= len(body):
|
|
69
|
+
match = _HOLLERITH_START_RE.search(body, position)
|
|
70
|
+
if match is None:
|
|
71
|
+
break
|
|
72
|
+
count = int(match.group(1))
|
|
73
|
+
start = match.end()
|
|
74
|
+
if start + count > len(body):
|
|
75
|
+
# the constant counts past this physical line — it continues on
|
|
76
|
+
# the next; rewriting it would move following line numbers
|
|
77
|
+
break
|
|
78
|
+
literal = body[start : start + count]
|
|
79
|
+
quoted = _quote(literal)
|
|
80
|
+
new_line = f"{body[: match.start()]}{quoted}{body[start + count :]}"
|
|
81
|
+
if len(new_line) > _FIXED_FORM_LINE_LIMIT:
|
|
82
|
+
# never widen the punch-card margin; leave this one alone
|
|
83
|
+
break
|
|
84
|
+
body = new_line
|
|
85
|
+
resolved += 1
|
|
86
|
+
position = match.start() + len(quoted)
|
|
87
|
+
return body + eol, resolved
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _quote(literal: str) -> str:
|
|
91
|
+
if "'" in literal:
|
|
92
|
+
return '"' + literal.replace('"', '""') + '"'
|
|
93
|
+
return "'" + literal + "'"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nativegate
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -552,3 +552,10 @@ mapping. Each refusal is printed at `generate` time with its reason, and the
|
|
|
552
552
|
generated service publishes them at `GET /_unexposed` so they stay visible to
|
|
553
553
|
whoever is calling the API. An empty mapping there means this build refused
|
|
554
554
|
nothing; a 404 means the service predates the route and cannot tell you.
|
|
555
|
+
|
|
556
|
+
## Community
|
|
557
|
+
|
|
558
|
+
Contributing: see the repository's [CONTRIBUTING.md](../../CONTRIBUTING.md).
|
|
559
|
+
Participation in this project's spaces is covered by the
|
|
560
|
+
[Code of Conduct](../../CODE_OF_CONDUCT.md); reports go to
|
|
561
|
+
sr.rabiu@gmail.com.
|
|
@@ -50,6 +50,7 @@ nativegate/parsers/fixed_form.py
|
|
|
50
50
|
nativegate/parsers/fortran.py
|
|
51
51
|
nativegate/parsers/fortran_fparser.py
|
|
52
52
|
nativegate/parsers/fortran_regex.py
|
|
53
|
+
nativegate/parsers/hollerith.py
|
|
53
54
|
nativegate/templates/__init__.py
|
|
54
55
|
nativegate/templates/golden_test_template.py
|
|
55
56
|
tests/test_buildinfo.py
|
|
@@ -68,6 +69,7 @@ tests/test_dialect.py
|
|
|
68
69
|
tests/test_docker_gen.py
|
|
69
70
|
tests/test_driverbuild.py
|
|
70
71
|
tests/test_endpoint_docstrings.py
|
|
72
|
+
tests/test_external_callbacks.py
|
|
71
73
|
tests/test_fixed_form.py
|
|
72
74
|
tests/test_fortran_doc_comments.py
|
|
73
75
|
tests/test_fortran_driver.py
|
|
@@ -77,6 +79,7 @@ tests/test_fortran_pipeline.py
|
|
|
77
79
|
tests/test_gateway_gen.py
|
|
78
80
|
tests/test_generated_python_is_valid.py
|
|
79
81
|
tests/test_golden.py
|
|
82
|
+
tests/test_hollerith.py
|
|
80
83
|
tests/test_inspect_dialect.py
|
|
81
84
|
tests/test_invariants.py
|
|
82
85
|
tests/test_ir_contract.py
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "nativegate"
|
|
16
|
-
version = "0.1.
|
|
16
|
+
version = "0.1.9"
|
|
17
17
|
description = "Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""D14: `external` dummies are callbacks — refusing to type them.
|
|
2
|
+
|
|
3
|
+
First contact: netlib quadpack QAGSE (f2py itself refuses — 'external f
|
|
4
|
+
is not in lcb_map[]'). The regex reader's IMPLICIT fallback typed 'f' as
|
|
5
|
+
`real` -> float: a binding that builds, imports, passes its smoke test
|
|
6
|
+
and hands the Fortran side garbage at call time. Both backends now
|
|
7
|
+
refuse the routine as a whole with the reason; ModuleIR.skipped carries
|
|
8
|
+
it. NOTE: the synthetic decks are written with hard six-space margins —
|
|
9
|
+
textwrap.dedent strips the shared indent and the files stop being
|
|
10
|
+
fixed-form.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from nativegate.config import ExposeConfig
|
|
16
|
+
from nativegate.parsers import fortran as fortran_parser
|
|
17
|
+
|
|
18
|
+
HEADER_SUB = (
|
|
19
|
+
" SUBROUTINE QNG(F, A, B)\n"
|
|
20
|
+
" EXTERNAL F\n"
|
|
21
|
+
" REAL A, B\n"
|
|
22
|
+
" END\n"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
HEADER_FREE = (
|
|
26
|
+
"subroutine qng(f, a, b)\n"
|
|
27
|
+
" external f\n"
|
|
28
|
+
" real a, b\n"
|
|
29
|
+
"end subroutine qng\n"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
HEADER_PLAIN = (
|
|
33
|
+
" SUBROUTINE ASSOC(A, B)\n"
|
|
34
|
+
" REAL A, B\n"
|
|
35
|
+
" A = B\n"
|
|
36
|
+
" RETURN\n"
|
|
37
|
+
" END\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_regex_backend_reports_external_dummy_as_skip(tmp_path):
|
|
42
|
+
src = tmp_path / "qng.f"
|
|
43
|
+
src.write_text(HEADER_SUB)
|
|
44
|
+
module = fortran_parser.parse_source(
|
|
45
|
+
src, ExposeConfig(functions=["QNG"]), backend="regex"
|
|
46
|
+
)
|
|
47
|
+
assert module.functions == []
|
|
48
|
+
assert len(module.skipped) == 1
|
|
49
|
+
assert module.skipped[0].name == "QNG"
|
|
50
|
+
assert "EXTERNAL" in module.skipped[0].reason
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_fparser_backend_reports_external_dummy_as_skip(tmp_path):
|
|
54
|
+
src = tmp_path / "qng.f"
|
|
55
|
+
src.write_text(HEADER_SUB)
|
|
56
|
+
module = fortran_parser.parse_source(
|
|
57
|
+
src, ExposeConfig(functions=["QNG"]), backend="fparser2"
|
|
58
|
+
)
|
|
59
|
+
assert module.functions == []
|
|
60
|
+
assert "EXTERNAL" in module.skipped[0].reason
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_free_form_backend_also_refuses_callbacks(tmp_path):
|
|
64
|
+
src = tmp_path / "qng.f90"
|
|
65
|
+
src.write_text(HEADER_FREE)
|
|
66
|
+
module = fortran_parser.parse_source(
|
|
67
|
+
src, ExposeConfig(functions=["qng"]), backend="regex"
|
|
68
|
+
)
|
|
69
|
+
assert module.functions == []
|
|
70
|
+
assert len(module.skipped) == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_routines_without_callbacks_still_bind(tmp_path):
|
|
74
|
+
src = tmp_path / "g.f"
|
|
75
|
+
src.write_text(HEADER_PLAIN)
|
|
76
|
+
module = fortran_parser.parse_source(
|
|
77
|
+
src, ExposeConfig(functions=["ASSOC"]), backend="regex"
|
|
78
|
+
)
|
|
79
|
+
assert [p.name.lower() for p in module.functions[0].parameters] == ["a", "b"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""D15: Hollerith constants resolve so fparser2 reads gfortran-acceptable files.
|
|
2
|
+
|
|
3
|
+
`call xerror(26habnormal return from qng ,26,ier,0)` compiles under
|
|
4
|
+
gfortran with a legacy-extension warning but made fparser2 refuse the
|
|
5
|
+
whole file — QUADPACK's QNG, un-inspectable at the default backend. The
|
|
6
|
+
front door now resolves these into the standard quoted spelling in a temp
|
|
7
|
+
copy attributed back to the original path (same module name, same source
|
|
8
|
+
file reference, same line count), so both `inspect` and `generate` see
|
|
9
|
+
the file the compiler sees.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import textwrap
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from click.testing import CliRunner
|
|
16
|
+
|
|
17
|
+
from nativegate import cli
|
|
18
|
+
from nativegate.parsers import fortran as fortran_parser
|
|
19
|
+
from nativegate.parsers import hollerith
|
|
20
|
+
from nativegate.config import ExposeConfig
|
|
21
|
+
|
|
22
|
+
QNG_WITH_HOLLERITH = (
|
|
23
|
+
" SUBROUTINE QNG(F, A, B)\n"
|
|
24
|
+
" EXTERNAL F\n"
|
|
25
|
+
" REAL A, B\n"
|
|
26
|
+
" IF (IER.NE.0) THEN\n"
|
|
27
|
+
" 80 CALL XERROR(26HABNORMAL RETURN FROM QNG ,26,IER,0)\n"
|
|
28
|
+
" END IF\n"
|
|
29
|
+
" RETURN\n"
|
|
30
|
+
" END\n"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_resolver_quotes_and_preserves_columns():
|
|
35
|
+
source = QNG_WITH_HOLLERITH
|
|
36
|
+
out, resolved = hollerith.resolve_hollerith(source)
|
|
37
|
+
assert resolved == 1
|
|
38
|
+
assert "26HABNORMAL" not in out
|
|
39
|
+
assert "'ABNORMAL RETURN FROM QNG '" in out
|
|
40
|
+
# line count and margin are invariants: every physical line stays
|
|
41
|
+
assert out.count("\n") == source.count("\n")
|
|
42
|
+
for original, rewritten in zip(source.splitlines(), out.splitlines()):
|
|
43
|
+
assert len(rewritten) <= 72
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_resolver_leaves_continuation_spanning_constants():
|
|
47
|
+
# the constant counts past the physical line: untouched, so line
|
|
48
|
+
# numbers never shift
|
|
49
|
+
text = " CALL XERROR(6HABCDE\n -F,6,IER,0)\n"
|
|
50
|
+
out, resolved = hollerith.resolve_hollerith(text)
|
|
51
|
+
assert resolved == 0
|
|
52
|
+
assert out == text
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_resolver_is_a_noop_without_hollerith():
|
|
56
|
+
text = " REAL A\n A = 2.0\n"
|
|
57
|
+
out, resolved = hollerith.resolve_hollerith(text)
|
|
58
|
+
assert resolved == 0 and out == text
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_quote_escapes_are_balanced():
|
|
62
|
+
assert hollerith._quote("it's") == '"it\'s"'
|
|
63
|
+
assert hollerith._quote('say "hi"') == '\'say "hi"\''
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_inspect_parses_hollerith_deck_by_default(tmp_path):
|
|
67
|
+
src = tmp_path / "qng.f"
|
|
68
|
+
src.write_text(QNG_WITH_HOLLERITH)
|
|
69
|
+
# the ONE check that matters: no explicit backend flag, the default
|
|
70
|
+
# (fparser2 where importable) reads the file the compiler reads
|
|
71
|
+
result = CliRunner().invoke(
|
|
72
|
+
cli.main, ["inspect", str(src), "--function", "QNG"]
|
|
73
|
+
)
|
|
74
|
+
assert result.exit_code == 0, result.output
|
|
75
|
+
# the callback is refused with its reason, never mis-typed (D14)
|
|
76
|
+
assert "EXTERNAL" in result.output
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_module_name_attributed_to_real_file(tmp_path):
|
|
80
|
+
src = tmp_path / "qng.f"
|
|
81
|
+
src.write_text(QNG_WITH_HOLLERITH)
|
|
82
|
+
module = fortran_parser.parse_source(src, ExposeConfig(functions=["QNG"]))
|
|
83
|
+
assert module.name == "qng"
|
|
84
|
+
assert module.source_file == str(src)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|