nativegate 0.1.2__tar.gz → 0.1.4__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 (106) hide show
  1. {nativegate-0.1.2 → nativegate-0.1.4}/PKG-INFO +1 -1
  2. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/cli.py +86 -4
  3. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/config.py +31 -0
  4. nativegate-0.1.4/nativegate/parsers/dialect.py +98 -0
  5. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/PKG-INFO +1 -1
  6. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/SOURCES.txt +3 -0
  7. {nativegate-0.1.2 → nativegate-0.1.4}/pyproject.toml +1 -1
  8. nativegate-0.1.4/tests/test_config_unknown_keys.py +77 -0
  9. nativegate-0.1.4/tests/test_dialect.py +184 -0
  10. {nativegate-0.1.2 → nativegate-0.1.4}/README.md +0 -0
  11. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/__init__.py +0 -0
  12. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/__main__.py +0 -0
  13. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/buildinfo.py +0 -0
  14. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/declared_invariants.py +0 -0
  15. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/discovery.py +0 -0
  16. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/driverbuild.py +0 -0
  17. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/drivers/__init__.py +0 -0
  18. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/drivers/cpp.py +0 -0
  19. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/drivers/fortran.py +0 -0
  20. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/__init__.py +0 -0
  21. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/cmake_gen.py +0 -0
  22. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/docker_gen.py +0 -0
  23. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/error_gen.py +0 -0
  24. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/f2py_gen.py +0 -0
  25. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/gateway_gen.py +0 -0
  26. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/golden_gen.py +0 -0
  27. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/k8s_gen.py +0 -0
  28. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/mcp_gen.py +0 -0
  29. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/middleware_gen.py +0 -0
  30. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/pybind_gen.py +0 -0
  31. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/pyproject_gen.py +0 -0
  32. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/python_pkg_gen.py +0 -0
  33. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/test_gen.py +0 -0
  34. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/golden.py +0 -0
  35. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/invariants.py +0 -0
  36. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/ir.py +0 -0
  37. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/lattice.py +0 -0
  38. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/locking.py +0 -0
  39. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/oracle.py +0 -0
  40. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/__init__.py +0 -0
  41. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/cpp.py +0 -0
  42. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/cpp_ast.py +0 -0
  43. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/cpp_regex.py +0 -0
  44. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fixed_form.py +0 -0
  45. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fortran.py +0 -0
  46. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fortran_fparser.py +0 -0
  47. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fortran_regex.py +0 -0
  48. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/preprocess.py +0 -0
  49. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/structural_invariants.py +0 -0
  50. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/suggest.py +0 -0
  51. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/templates/__init__.py +0 -0
  52. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/templates/golden_test_template.py +0 -0
  53. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/wire.py +0 -0
  54. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/dependency_links.txt +0 -0
  55. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/entry_points.txt +0 -0
  56. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/requires.txt +0 -0
  57. {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/top_level.txt +0 -0
  58. {nativegate-0.1.2 → nativegate-0.1.4}/setup.cfg +0 -0
  59. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_buildinfo.py +0 -0
  60. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cli_serve_verify.py +0 -0
  61. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cli_suggest.py +0 -0
  62. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cli_verify_aggregate.py +0 -0
  63. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cmake_gen.py +0 -0
  64. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cpp_ast.py +0 -0
  65. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cpp_driver.py +0 -0
  66. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cpp_pipeline.py +0 -0
  67. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_declared_invariants.py +0 -0
  68. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_derived_types.py +0 -0
  69. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_docker_gen.py +0 -0
  70. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_driverbuild.py +0 -0
  71. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_endpoint_docstrings.py +0 -0
  72. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fixed_form.py +0 -0
  73. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_doc_comments.py +0 -0
  74. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_driver.py +0 -0
  75. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_fparser.py +0 -0
  76. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_libraries.py +0 -0
  77. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_pipeline.py +0 -0
  78. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_gateway_gen.py +0 -0
  79. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_generated_python_is_valid.py +0 -0
  80. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_golden.py +0 -0
  81. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_invariants.py +0 -0
  82. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_ir_contract.py +0 -0
  83. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_k8s_gen.py +0 -0
  84. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_lattice.py +0 -0
  85. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_legacy_cpp.py +0 -0
  86. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_legacy_fortran.py +0 -0
  87. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_locking.py +0 -0
  88. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_mcp_gen.py +0 -0
  89. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_middleware_gen.py +0 -0
  90. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_multifile_cpp.py +0 -0
  91. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_native_oracle.py +0 -0
  92. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_native_state_contract.py +0 -0
  93. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_oracle.py +0 -0
  94. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_preprocess_and_config.py +0 -0
  95. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_preprocessed_fortran.py +0 -0
  96. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_pybind_gen.py +0 -0
  97. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_pyproject_gen.py +0 -0
  98. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_quickstart.py +0 -0
  99. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_scalar_ref.py +0 -0
  100. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_service_gen.py +0 -0
  101. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_service_name_validation.py +0 -0
  102. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_shared_libraries.py +0 -0
  103. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_structural_invariants.py +0 -0
  104. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_suggest.py +0 -0
  105. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_verification_config.py +0 -0
  106. {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_wire.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nativegate
3
- Version: 0.1.2
3
+ Version: 0.1.4
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
@@ -69,6 +69,7 @@ from .parsers import cpp as cpp_parser
69
69
  from .parsers.cpp_ast import ClangOptions
70
70
  from .parsers import fixed_form
71
71
  from .parsers import fortran as fortran_parser
72
+ from .parsers.dialect import apply_dialect, is_dialect_marked, resolve_dialect
72
73
 
73
74
  SERVICES_DIR = "services"
74
75
 
@@ -244,7 +245,15 @@ def create_service(name: str, language: str, force: bool) -> None:
244
245
  help="Delete and recreate services/<name> if it already exists.",
245
246
  )
246
247
  @click.option("--build/--no-build", default=False, help="Also compile the extension after generating.")
247
- def quickstart(source: Path, name: str | None, force: bool, build: bool) -> None:
248
+ @click.option(
249
+ "--dialect",
250
+ default="",
251
+ help="Select the live half of netlib-style dual-dialect sources "
252
+ "(CS/CD machine-precision marking): 'cs' or 'cd'. The resolved copy "
253
+ "goes to native/_expanded; the source under native/ is untouched. "
254
+ "Leave empty for sources that are not dialect-marked.",
255
+ )
256
+ def quickstart(source: Path, name: str | None, force: bool, build: bool, dialect: str) -> None:
248
257
  """One-shot: scaffold a service from a single C++/Fortran file, expose
249
258
  everything found in it, and generate the Python package — no manual
250
259
  nativegate.yaml editing.
@@ -310,10 +319,51 @@ def quickstart(source: Path, name: str | None, force: bool, build: bool) -> None
310
319
  f"{service_dir / 'native'} and re-run `ngate generate {service_name}`."
311
320
  )
312
321
  else:
313
- routines = fortran_parser.list_routine_names(native_copy)
322
+ resolved = native_copy
323
+ try:
324
+ config.dialect = resolve_dialect(dialect)
325
+ except ValueError as exc:
326
+ tracker.fail("Copy native source")
327
+ raise click.ClickException(str(exc)) from exc
328
+ if config.dialect and is_dialect_marked(read_source(native_copy)):
329
+ resolved_dir = service_dir / "native" / "_expanded"
330
+ resolved_dir.mkdir(parents=True, exist_ok=True)
331
+ resolved = resolved_dir / native_copy.name
332
+ resolved.write_text(apply_dialect(read_source(native_copy), config.dialect))
333
+ tracker.log(
334
+ f"dialect '{config.dialect}' selected for {native_copy.name} "
335
+ f"-> {resolved} (native/ keeps the untouched upstream bytes)"
336
+ )
337
+ else:
338
+ config.dialect = ""
339
+ try:
340
+ routines = fortran_parser.list_routine_names(resolved)
341
+ except ValueError as exc:
342
+ # Why not pass the parser's message through untouched: for a
343
+ # netlib CS/CD dual-dialect deck the parser error ("floating
344
+ # continuation") is real but does not name the fix, which is
345
+ # one flag on this very command.
346
+ hint = ""
347
+ if not config.dialect and is_dialect_marked(read_source(native_copy)):
348
+ hint = (
349
+ "\nThis file looks dialect-marked (CS/CD prefix on both "
350
+ "a single- and a double-precision copy of every line). "
351
+ "Pass '--dialect cs' or '--dialect cd' to select the "
352
+ "live half."
353
+ )
354
+ tracker.fail("Copy native source")
355
+ raise click.ClickException(f"{exc}{hint}") from exc
314
356
  if not routines:
315
357
  tracker.fail("Copy native source")
316
- raise click.ClickException(f"No Fortran function/subroutine declarations found in {source}.")
358
+ hint = ""
359
+ if not config.dialect and is_dialect_marked(read_source(native_copy)):
360
+ hint = (
361
+ " (The source carries netlib CS/CD dual-dialect marking: "
362
+ "pass --dialect cs or --dialect cd to select the live half.)"
363
+ )
364
+ raise click.ClickException(
365
+ f"No Fortran function/subroutine declarations found in {source}.{hint}"
366
+ )
317
367
  config.expose.functions = routines
318
368
  config.save(service_dir)
319
369
  tracker.log(f"Exposing Fortran routines: {', '.join(routines)}")
@@ -1719,6 +1769,35 @@ def _generate_fortran_service(service_dir: Path, config: ServiceConfig) -> None:
1719
1769
  if not sources:
1720
1770
  raise click.ClickException(f"No Fortran sources found under {service_dir / 'native'}.")
1721
1771
 
1772
+ # netlib dual-dialect sources (CS/CD stamped on both halves' every line)
1773
+ # are not parseable by anything that reads real Fortran — see
1774
+ # DEFECTS D10. The selected dialect's lines become the code, the other
1775
+ # becomes a comment, and the resolved copy lives in `_expanded` next
1776
+ # to the other generated transforms. Done at the very front, so every
1777
+ # reader of a source (routine discovery, the parse, intent inference,
1778
+ # f2py) sees the same resolved file.
1779
+ dialect_set = set()
1780
+ if config.dialect:
1781
+ chosen = [s for s in sources if is_dialect_marked(s.read_text())]
1782
+ expanded_dir = service_dir / "native" / "_expanded"
1783
+ expanded_dir.mkdir(parents=True, exist_ok=True)
1784
+ replaced: list[Path] = []
1785
+ for source in sources:
1786
+ if source not in chosen:
1787
+ replaced.append(source)
1788
+ continue
1789
+ target = expanded_dir / source.name
1790
+ target.write_text(apply_dialect(read_source(source), config.dialect))
1791
+ replaced.append(target)
1792
+ dialect_set.add(target)
1793
+ sources = replaced
1794
+ if chosen:
1795
+ click.echo(
1796
+ f"Resolved dialect '{config.dialect}' in {len(chosen)} source(s) "
1797
+ f"-> native/_expanded. The '{config.dialect}' half is now live; "
1798
+ "the other half is commented. Originals under native/ are untouched."
1799
+ )
1800
+
1722
1801
  include_paths = [Path(p) for p in config.include_paths]
1723
1802
  for p in include_paths:
1724
1803
  if not p.is_dir():
@@ -1910,13 +1989,16 @@ def _generate_fortran_service(service_dir: Path, config: ServiceConfig) -> None:
1910
1989
  + kind_param_sources
1911
1990
  + shim_only_sources
1912
1991
  )
1992
+ # Dialect-resolved copies live under _expanded by construction, so the
1993
+ # referenced tree must be built even when nothing else needs rewriting.
1994
+ rewritten_or_copied = bool(rewritten) or bool(library_sources) or bool(preprocessed_set) or bool(dialect_set)
1913
1995
  # A library source is always copied under the service, even when it needs
1914
1996
  # no rewriting: it lives outside native/, so referencing it as
1915
1997
  # `native/<name>` would name a file that is not there — and the Docker
1916
1998
  # build context is the service directory, so a path outside it could not be
1917
1999
  # COPYed in anyway.
1918
2000
  library_set = set(library_sources)
1919
- if rewritten or library_sources or preprocessed_set:
2001
+ if rewritten_or_copied:
1920
2002
  expanded_dir = service_dir / "native" / "_expanded"
1921
2003
  expanded_dir.mkdir(parents=True, exist_ok=True)
1922
2004
  native_sources = []
@@ -10,6 +10,8 @@ from typing import TYPE_CHECKING, Union
10
10
 
11
11
  import yaml
12
12
 
13
+ from .parsers.dialect import resolve_dialect
14
+
13
15
  if TYPE_CHECKING:
14
16
  from .ir import ModuleIR
15
17
 
@@ -783,6 +785,12 @@ class ServiceConfig:
783
785
  # the `_expanded` copy. Same reason clang.defines exists for C++: the
784
786
  # branches differ, and only the build knows which one is live.
785
787
  fortran_defines: list[str] = field(default_factory=list)
788
+ # Dialect selection for netlib-style dual-dialect sources ("cs" or
789
+ # "cd"), applied when a file is parsed AND before f2py compiles it.
790
+ # Empty means no source carries the marking; discriminating per-file
791
+ # is done by `is_dialect_marked`, so one value safely covers a mixed
792
+ # tree.
793
+ dialect: str = ""
786
794
  # `state:`/`invariants:`/`ranges:` — layer 3 declarations
787
795
  # (design-verification-layers.md §3.2-3.5). Optional: a service with no
788
796
  # invariants.json coverage simply has an empty VerificationConfig.
@@ -797,6 +805,26 @@ class ServiceConfig:
797
805
  "Run `ngate create-service` first."
798
806
  )
799
807
  data = yaml.safe_load(config_path.read_text()) or {}
808
+ # Unknown top-level keys are an error, not dead weight. A new
809
+ # nativegate.yaml key read by an older nativegate (the exact case
810
+ # this project shipped CI against: `dialect: cd` under a nativegate
811
+ # that pre-dated it) would otherwise vanish the same way a letter
812
+ # transposed in `dialete:` would — the service generates from an
813
+ # assumption the user never wrote.
814
+ known_keys = {
815
+ "name", "language", "expose", "parser", "clang", "libraries",
816
+ "include_paths", "api", "dialect",
817
+ "fortran", # namespace for fortran.defines
818
+ # verification layer 3 keys
819
+ "state", "invariants", "ranges", "lattice",
820
+ }
821
+ unknown = sorted(set(data) - known_keys)
822
+ if unknown:
823
+ raise ConfigError(
824
+ f"{config_path}: unknown key(s) {', '.join(unknown)}. "
825
+ "Check the spelling against this nativegate version's supported "
826
+ "keys, or upgrade nativegate if the key is new."
827
+ )
800
828
  expose = _load_expose(data.get("expose"), config_path)
801
829
  clang_data = data.get("clang") or {}
802
830
  # `None` means "no restriction": `expose: all` or an empty `expose:`
@@ -819,6 +847,7 @@ class ServiceConfig:
819
847
  ),
820
848
  libraries=list(data.get("libraries") or []),
821
849
  include_paths=list(data.get("include_paths") or []),
850
+ dialect=resolve_dialect(data.get("dialect")),
822
851
  api=_load_api(data.get("api"), config_path),
823
852
  fortran_defines=list((data.get("fortran") or {}).get("defines") or []),
824
853
  verification=_load_verification(data, config_path, known_functions),
@@ -853,6 +882,8 @@ class ServiceConfig:
853
882
  data["libraries"] = self.libraries
854
883
  if self.include_paths:
855
884
  data["include_paths"] = self.include_paths
885
+ if self.dialect:
886
+ data["dialect"] = self.dialect
856
887
  # Only written when it differs from the default, so an existing
857
888
  # nativegate.yaml does not grow a key that says nothing.
858
889
  if self.api.auth != "none":
@@ -0,0 +1,98 @@
1
+ """Dialect selection for netlib-style dual-dialect Fortran (DEFECTS D10).
2
+
3
+ Some netlib-era sources (specfun 2.5 is the canonical case, 1996) mark
4
+ machine precision by commenting EVERY statement in both dialects and
5
+ asking the installer to comment one batch out manually:
6
+
7
+ CS REAL FUNCTION GAMMA(X)
8
+ CD DOUBLE PRECISION FUNCTION DGAMMA(X)
9
+ CS REAL
10
+ CD DOUBLE PRECISION
11
+ 1 C,CONV,EPS, ... <- no statement starts in column 7
12
+
13
+ With neither prefix commented, no standard parser can read the file: the
14
+ grammar has a floating continuation with no statement behind it, and
15
+ gfortran rejects it too ("no implicit type at 'CS'..." paired with the
16
+ dangling continuation). The `NATIVEGATE_FORTRAN_PARSER=regex` fallback
17
+ does not help — the FUNCTION statements are marked the same way.
18
+
19
+ `apply_dialect` makes the choice static in place: the selected prefix is
20
+ uncommented (its two characters become blank columns) and the other
21
+ prefix is pushed under a comment (`C` + whatever remained). Column
22
+ positions are preserved, which is what fixed-form requires: `CS 1 `
23
+ (col 3-6 blank, 1 in column 6) and `CD` lines follow the same geometry.
24
+
25
+ Idempotent: after one pass no line starts CS/CD any more, so re-running
26
+ on an already-resolved file is a no-op, which is what makes it safe for
27
+ generate to apply it to every source without knowing which ones are
28
+ marked.
29
+
30
+ The transform is deliberately pure text on the fixed-form column grid:
31
+ uncommenting replaces the two marker characters with two blanks, so no
32
+ column ever shifts, and commenting restores the `C` in column 1. Only
33
+ `native/_expanded/` carries the resolved file — the byte-for-byte copy
34
+ under `native/native/...` is untouched, so provenance (`golden.json`
35
+ hashes) keeps describing the upstream source, not the transform output.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import re
41
+
42
+ DIALECT_CHOICES = ("cs", "cd")
43
+
44
+ _PREFIX_RE = re.compile(r"^(CS|CD)(?=[ \t])", re.IGNORECASE | re.MULTILINE)
45
+
46
+
47
+ def resolve_dialect(raw: str | None) -> str:
48
+ """Validate a `dialect:` value from nativegate.yaml / --dialect.
49
+
50
+ `""` means "the source is not dialect-marked, leave everything alone".
51
+ Anything unlisted is an error, not a silent no-op — a typo'd
52
+ `dialect: dc` must fail here, not turn into "unresolved source".
53
+ """
54
+ value = (raw or "").strip().lower()
55
+ if value and value not in DIALECT_CHOICES:
56
+ raise ValueError(
57
+ f"Unknown dialect '{value}'; expected one of: "
58
+ + ", ".join(DIALECT_CHOICES) + " (or empty for none)."
59
+ )
60
+ return value.lower()
61
+
62
+
63
+ def is_dialect_marked(text: str) -> bool:
64
+ """True when any expected prefix appears at a statement column.
65
+
66
+ Deliberately coarse: a file with the markers gets resolved from the
67
+ start, whether or not every line carries them. A single CS/CD word
68
+ that opens a line is a genuine dialect marker in practice — no Fortran
69
+ statement begins with those letters in column 1.
70
+ """
71
+ return bool(_PREFIX_RE.search(text))
72
+
73
+
74
+ def apply_dialect(text: str, dialect: str) -> str:
75
+ """Resolve the dual-dialect marking to a single live dialect.
76
+
77
+ `dialect` is the prefix that becomes the code; the other becomes a
78
+ comment (`C ` + the remainder), so the rejected variant stays readable
79
+ in place — the same shape the files will have after a human editor
80
+ does the step netlib's readme prescribes.
81
+ """
82
+ dialect = resolve_dialect(dialect)
83
+ if not dialect or not is_dialect_marked(text):
84
+ return text
85
+
86
+ selected = dialect.upper()
87
+ out = []
88
+ for line in text.splitlines(keepends=True):
89
+ match = _PREFIX_RE.match(line)
90
+ if not match:
91
+ out.append(line)
92
+ continue
93
+ marker = match.group(1).upper()
94
+ if marker == selected:
95
+ out.append(" " + line[2:])
96
+ else:
97
+ out.append("C " + line[2:])
98
+ return "".join(out)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nativegate
3
- Version: 0.1.2
3
+ Version: 0.1.4
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
@@ -45,6 +45,7 @@ nativegate/parsers/__init__.py
45
45
  nativegate/parsers/cpp.py
46
46
  nativegate/parsers/cpp_ast.py
47
47
  nativegate/parsers/cpp_regex.py
48
+ nativegate/parsers/dialect.py
48
49
  nativegate/parsers/fixed_form.py
49
50
  nativegate/parsers/fortran.py
50
51
  nativegate/parsers/fortran_fparser.py
@@ -56,11 +57,13 @@ tests/test_cli_serve_verify.py
56
57
  tests/test_cli_suggest.py
57
58
  tests/test_cli_verify_aggregate.py
58
59
  tests/test_cmake_gen.py
60
+ tests/test_config_unknown_keys.py
59
61
  tests/test_cpp_ast.py
60
62
  tests/test_cpp_driver.py
61
63
  tests/test_cpp_pipeline.py
62
64
  tests/test_declared_invariants.py
63
65
  tests/test_derived_types.py
66
+ tests/test_dialect.py
64
67
  tests/test_docker_gen.py
65
68
  tests/test_driverbuild.py
66
69
  tests/test_endpoint_docstrings.py
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
 
14
14
  [project]
15
15
  name = "nativegate"
16
- version = "0.1.2"
16
+ version = "0.1.4"
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,77 @@
1
+ """Unknown top-level keys in nativegate.yaml must fail loading.
2
+
3
+ Found in the wild (2026-09-23): specfun-py's CI installed a nativegate
4
+ that pre-dated the `dialect:` key and generated the service anyway from
5
+ an assumption never written down — the unknown key was swallowed as dead
6
+ weight. A typo in any key was equally silent. Now both are config errors
7
+ that say what is wrong and what to do about it.
8
+ """
9
+
10
+ import shutil
11
+ import textwrap
12
+
13
+ import pytest
14
+
15
+ from nativegate.config import ConfigError, ServiceConfig
16
+
17
+
18
+ @pytest.fixture()
19
+ def scratch(tmp_path, monkeypatch):
20
+ service = tmp_path / "demo"
21
+ (service / "native").mkdir(parents=True)
22
+ monkeypatch.chdir(tmp_path)
23
+ return service
24
+
25
+
26
+ def _config_for_lines(text):
27
+ def _load(service):
28
+ (service / "nativegate.yaml").write_text(text)
29
+ return ServiceConfig.load(service)
30
+ return _load
31
+
32
+
33
+ def test_unknown_key_is_an_error(scratch):
34
+ (scratch / "nativegate.yaml").write_text(textwrap.dedent("""\
35
+ name: demo
36
+ language: fortran
37
+ diatect: cd
38
+ expose:
39
+ functions: []
40
+ """))
41
+ with pytest.raises(ConfigError) as exc:
42
+ ServiceConfig.load(scratch)
43
+ assert "diatect" in str(exc.value)
44
+ assert "upgrade nativegate" in str(exc.value)
45
+
46
+
47
+ def test_future_key_is_a_config_error_not_a_silent_noop(scratch):
48
+ (scratch / "nativegate.yaml").write_text(textwrap.dedent("""\
49
+ name: demo
50
+ language: fortran
51
+ dialect: cd
52
+ expose:
53
+ functions: []
54
+ """))
55
+ # fine on a nativegate that knows the key
56
+ config = ServiceConfig.load(scratch)
57
+ assert config.dialect == "cd"
58
+
59
+
60
+ def test_all_declared_keys_still_load(scratch):
61
+ (scratch / "nativegate.yaml").write_text(textwrap.dedent("""\
62
+ name: demo
63
+ language: fortran
64
+ dialect: cd
65
+ parser: auto
66
+ libraries:
67
+ - petro
68
+ include_paths: []
69
+ fortran:
70
+ defines: []
71
+ expose:
72
+ all: true
73
+ """))
74
+ config = ServiceConfig.load(scratch)
75
+ assert config.dialect == "cd"
76
+ assert config.libraries == ["petro"]
77
+ assert config.expose.all is True
@@ -0,0 +1,184 @@
1
+ """D10: the netlib CS/CD dual-dialect marking.
2
+
3
+ Sources fetched straight from netlib specfun carry every statement twice,
4
+ in both dialects, comment-marked by prefix (CS = single, CD = double), and
5
+ the installer comments one batch out by hand. Neither parser could read
6
+ such a file — the fixed-form grammar sees a continuation with no statement,
7
+ and the regex fallback finds no routine because the FUNCTION lines are
8
+ marked too. The fix is `dialect:` in nativegate.yaml / `--dialect` on
9
+ quickstart: the chosen prefix's lines become the code, the other prefix's
10
+ lines become comments, and the resolved copy lives under native/_expanded
11
+ so the untouched upstream bytes stay in native/.
12
+ """
13
+
14
+ import textwrap
15
+
16
+ import pytest
17
+ from click.testing import CliRunner
18
+
19
+ from nativegate import cli
20
+ from nativegate.parsers.dialect import (
21
+ apply_dialect,
22
+ is_dialect_marked,
23
+ resolve_dialect,
24
+ )
25
+
26
+
27
+ # --- unit ----------------------------------------------------------------
28
+
29
+
30
+ def test_resolve_dialect_validates():
31
+ assert resolve_dialect("") == ""
32
+ assert resolve_dialect("CS") == "cs"
33
+ assert resolve_dialect("cd") == "cd"
34
+ with pytest.raises(ValueError):
35
+ resolve_dialect("dc") # plausible typo must fail, not act as a no-op
36
+ with pytest.raises(ValueError):
37
+ resolve_dialect("both")
38
+
39
+
40
+ DUAL_DIALECT = textwrap.dedent("""\
41
+ CS REAL FUNCTION GAMMA(X)
42
+ CD DOUBLE PRECISION FUNCTION DGAMMA(X)
43
+ CS REAL
44
+ CD DOUBLE PRECISION
45
+ 1 C,CONV,EPS
46
+ DGAMMA = 24.0D0
47
+ RETURN
48
+ END
49
+ """)
50
+
51
+
52
+ def test_apply_dialect_selects_cd():
53
+ out = apply_dialect(DUAL_DIALECT, "cd")
54
+ lines = out.splitlines()
55
+ # the chosen CD lines are live, shifted two columns into statement
56
+ # position, with no other column moved
57
+ assert " DOUBLE PRECISION FUNCTION DGAMMA(X)" in lines
58
+ assert " DOUBLE PRECISION" in lines
59
+ # the rejected CS half is commented back to the normal fixed-form
60
+ # comment form, `C` in column 1
61
+ assert "C REAL FUNCTION GAMMA(X)" in lines
62
+ assert "C REAL" in lines
63
+ # untouched lines are untouched
64
+ assert " DGAMMA = 24.0D0" in lines
65
+ assert " 1 C,CONV,EPS" in lines
66
+
67
+
68
+ def test_apply_dialect_selects_cs():
69
+ out = apply_dialect(DUAL_DIALECT, "cs")
70
+ lines = out.splitlines()
71
+ assert " REAL FUNCTION GAMMA(X)" in lines
72
+ assert " REAL" in lines
73
+ commented = [line for line in lines if line.startswith("C ")]
74
+ assert any("DGAMMA" in line and "DOUBLE PRECISION FUNCTION" in line
75
+ for line in commented)
76
+
77
+
78
+ def test_apply_dialect_leaves_plain_source_alone():
79
+ plain = " DOUBLE PRECISION FUNCTION DGAMMA(X)\n END\n"
80
+ assert apply_dialect(plain, "cd") == plain
81
+ assert apply_dialect(plain, "") == plain
82
+
83
+
84
+ def test_apply_dialect_is_idempotent():
85
+ once = apply_dialect(DUAL_DIALECT, "cd")
86
+ assert apply_dialect(once, "cd") == once
87
+ assert not is_dialect_marked(once)
88
+
89
+
90
+ def test_is_dialect_marked():
91
+ assert is_dialect_marked(DUAL_DIALECT)
92
+ assert not is_dialect_marked(apply_dialect(DUAL_DIALECT, "cd"))
93
+ assert not is_dialect_marked("")
94
+
95
+
96
+ def test_apply_dialect_rejects_bad_dialect():
97
+ with pytest.raises(ValueError):
98
+ apply_dialect(DUAL_DIALECT, "dc")
99
+
100
+
101
+ # --- config round-trip -----------------------------------------------------
102
+
103
+
104
+ def test_config_round_trips_dialect(tmp_path):
105
+ """`dialect:` survives a save/load cycle, and an unknown value is a
106
+ config error rather than a silently ignored key."""
107
+ import yaml
108
+ from nativegate.config import ServiceConfig
109
+
110
+ service = tmp_path / "specfun_gamma"
111
+ service.mkdir(parents=True)
112
+ (service / "nativegate.yaml").write_text(
113
+ yaml.dump({"name": "specfun_gamma", "language": "fortran",
114
+ "dialect": "cd", "expose": {"functions": []}},
115
+ sort_keys=False),
116
+ )
117
+ config = ServiceConfig.load(service)
118
+ assert config.dialect == "cd"
119
+
120
+ config.save(service)
121
+ reloaded = ServiceConfig.load(service)
122
+ assert reloaded.dialect == "cd"
123
+
124
+ (service / "nativegate.yaml").write_text(
125
+ yaml.dump({"name": "specfun_gamma", "language": "fortran",
126
+ "dialect": "dc", "expose": {}},
127
+ sort_keys=False),
128
+ )
129
+ with pytest.raises(Exception, match="dialect"):
130
+ ServiceConfig.load(service)
131
+
132
+
133
+ # --- CLI wiring -----------------------------------------------------------
134
+
135
+
136
+ @pytest.fixture()
137
+ def netlib_scratch(tmp_path, monkeypatch):
138
+ source = tmp_path / "gamma.f"
139
+ source.write_text(DUAL_DIALECT)
140
+ monkeypatch.chdir(tmp_path)
141
+ return source
142
+
143
+
144
+ def test_quickstart_dialect_generates_and_keeps_native_untouched(netlib_scratch, tmp_path):
145
+ result = CliRunner().invoke(
146
+ cli.main,
147
+ ["quickstart", str(netlib_scratch), "--name", "specfun_gamma",
148
+ "--dialect", "cd"],
149
+ )
150
+ assert result.exit_code == 0, result.output
151
+
152
+ service = tmp_path / "services" / "specfun_gamma"
153
+ native = service / "native" / "gamma.f"
154
+ assert native.read_text() == DUAL_DIALECT
155
+ resolved = service / "native" / "_expanded" / "gamma.f"
156
+ assert resolved.exists()
157
+ assert resolved.read_text().rstrip("\n") == apply_dialect(
158
+ DUAL_DIALECT, "cd"
159
+ ).rstrip("\n")
160
+
161
+ assert "dialect: cd" in (service / "nativegate.yaml").read_text()
162
+
163
+ init = service / "python" / "specfun_gamma" / "__init__.py"
164
+ assert "dgamma" in init.read_text().lower()
165
+
166
+
167
+ def test_without_dialect_the_marked_file_fails_with_a_direction(netlib_scratch):
168
+ result = CliRunner().invoke(
169
+ cli.main, ["quickstart", str(netlib_scratch), "--name", "specfun_gamma"]
170
+ )
171
+ assert result.exit_code != 0
172
+ # the parser error is passed through, but the remedy is named
173
+ assert "dialect-marked" in result.output
174
+ assert "--dialect cs" in result.output and "--dialect cd" in result.output
175
+
176
+
177
+ def test_bad_dialect_is_an_error_not_a_noop(netlib_scratch):
178
+ result = CliRunner().invoke(
179
+ cli.main,
180
+ ["quickstart", str(netlib_scratch), "--name", "specfun_gamma",
181
+ "--dialect", "dc"],
182
+ )
183
+ assert result.exit_code != 0
184
+ assert "Unknown dialect" in result.output
File without changes
File without changes
File without changes