nativegate 0.1.8__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.
Files changed (111) hide show
  1. {nativegate-0.1.8 → nativegate-0.1.9}/PKG-INFO +8 -1
  2. {nativegate-0.1.8 → nativegate-0.1.9}/README.md +7 -0
  3. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/fortran.py +42 -0
  4. nativegate-0.1.9/nativegate/parsers/hollerith.py +93 -0
  5. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate.egg-info/PKG-INFO +8 -1
  6. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate.egg-info/SOURCES.txt +2 -0
  7. {nativegate-0.1.8 → nativegate-0.1.9}/pyproject.toml +1 -1
  8. nativegate-0.1.9/tests/test_hollerith.py +84 -0
  9. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/__init__.py +0 -0
  10. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/__main__.py +0 -0
  11. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/buildinfo.py +0 -0
  12. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/cli.py +0 -0
  13. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/config.py +0 -0
  14. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/declared_invariants.py +0 -0
  15. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/discovery.py +0 -0
  16. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/driverbuild.py +0 -0
  17. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/drivers/__init__.py +0 -0
  18. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/drivers/cpp.py +0 -0
  19. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/drivers/fortran.py +0 -0
  20. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/__init__.py +0 -0
  21. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/cmake_gen.py +0 -0
  22. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/docker_gen.py +0 -0
  23. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/error_gen.py +0 -0
  24. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/f2py_gen.py +0 -0
  25. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/gateway_gen.py +0 -0
  26. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/golden_gen.py +0 -0
  27. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/k8s_gen.py +0 -0
  28. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/mcp_gen.py +0 -0
  29. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/middleware_gen.py +0 -0
  30. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/pybind_gen.py +0 -0
  31. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/pyproject_gen.py +0 -0
  32. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/python_pkg_gen.py +0 -0
  33. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/generators/test_gen.py +0 -0
  34. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/golden.py +0 -0
  35. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/invariants.py +0 -0
  36. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/ir.py +0 -0
  37. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/lattice.py +0 -0
  38. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/locking.py +0 -0
  39. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/oracle.py +0 -0
  40. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/__init__.py +0 -0
  41. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/cpp.py +0 -0
  42. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/cpp_ast.py +0 -0
  43. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/cpp_regex.py +0 -0
  44. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/dialect.py +0 -0
  45. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/fixed_form.py +0 -0
  46. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/fortran_fparser.py +0 -0
  47. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/parsers/fortran_regex.py +0 -0
  48. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/preprocess.py +0 -0
  49. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/structural_invariants.py +0 -0
  50. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/suggest.py +0 -0
  51. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/templates/__init__.py +0 -0
  52. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/templates/golden_test_template.py +0 -0
  53. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate/wire.py +0 -0
  54. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate.egg-info/dependency_links.txt +0 -0
  55. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate.egg-info/entry_points.txt +0 -0
  56. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate.egg-info/requires.txt +0 -0
  57. {nativegate-0.1.8 → nativegate-0.1.9}/nativegate.egg-info/top_level.txt +0 -0
  58. {nativegate-0.1.8 → nativegate-0.1.9}/setup.cfg +0 -0
  59. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_buildinfo.py +0 -0
  60. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cli_serve_verify.py +0 -0
  61. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cli_suggest.py +0 -0
  62. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cli_verify_aggregate.py +0 -0
  63. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cmake_gen.py +0 -0
  64. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cmake_missing_input.py +0 -0
  65. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_config_unknown_keys.py +0 -0
  66. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cpp_ast.py +0 -0
  67. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cpp_driver.py +0 -0
  68. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_cpp_pipeline.py +0 -0
  69. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_declared_invariants.py +0 -0
  70. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_derived_types.py +0 -0
  71. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_dialect.py +0 -0
  72. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_docker_gen.py +0 -0
  73. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_driverbuild.py +0 -0
  74. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_endpoint_docstrings.py +0 -0
  75. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_external_callbacks.py +0 -0
  76. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_fixed_form.py +0 -0
  77. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_fortran_doc_comments.py +0 -0
  78. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_fortran_driver.py +0 -0
  79. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_fortran_fparser.py +0 -0
  80. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_fortran_libraries.py +0 -0
  81. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_fortran_pipeline.py +0 -0
  82. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_gateway_gen.py +0 -0
  83. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_generated_python_is_valid.py +0 -0
  84. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_golden.py +0 -0
  85. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_inspect_dialect.py +0 -0
  86. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_invariants.py +0 -0
  87. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_ir_contract.py +0 -0
  88. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_k8s_gen.py +0 -0
  89. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_lattice.py +0 -0
  90. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_legacy_cpp.py +0 -0
  91. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_legacy_fortran.py +0 -0
  92. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_locking.py +0 -0
  93. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_mcp_gen.py +0 -0
  94. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_middleware_gen.py +0 -0
  95. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_multifile_cpp.py +0 -0
  96. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_native_oracle.py +0 -0
  97. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_native_state_contract.py +0 -0
  98. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_oracle.py +0 -0
  99. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_preprocess_and_config.py +0 -0
  100. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_preprocessed_fortran.py +0 -0
  101. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_pybind_gen.py +0 -0
  102. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_pyproject_gen.py +0 -0
  103. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_quickstart.py +0 -0
  104. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_scalar_ref.py +0 -0
  105. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_service_gen.py +0 -0
  106. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_service_name_validation.py +0 -0
  107. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_shared_libraries.py +0 -0
  108. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_structural_invariants.py +0 -0
  109. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_suggest.py +0 -0
  110. {nativegate-0.1.8 → nativegate-0.1.9}/tests/test_verification_config.py +0 -0
  111. {nativegate-0.1.8 → 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.8
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)
@@ -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.8
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
@@ -78,6 +79,7 @@ tests/test_fortran_pipeline.py
78
79
  tests/test_gateway_gen.py
79
80
  tests/test_generated_python_is_valid.py
80
81
  tests/test_golden.py
82
+ tests/test_hollerith.py
81
83
  tests/test_inspect_dialect.py
82
84
  tests/test_invariants.py
83
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.8"
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,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