nativegate 0.1.1__tar.gz → 0.1.3__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 (105) hide show
  1. {nativegate-0.1.1 → nativegate-0.1.3}/PKG-INFO +1 -1
  2. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/cli.py +103 -4
  3. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/config.py +11 -0
  4. nativegate-0.1.3/nativegate/parsers/dialect.py +98 -0
  5. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/PKG-INFO +1 -1
  6. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/SOURCES.txt +3 -0
  7. {nativegate-0.1.1 → nativegate-0.1.3}/pyproject.toml +1 -1
  8. nativegate-0.1.3/tests/test_dialect.py +184 -0
  9. nativegate-0.1.3/tests/test_service_name_validation.py +47 -0
  10. {nativegate-0.1.1 → nativegate-0.1.3}/README.md +0 -0
  11. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/__init__.py +0 -0
  12. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/__main__.py +0 -0
  13. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/buildinfo.py +0 -0
  14. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/declared_invariants.py +0 -0
  15. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/discovery.py +0 -0
  16. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/driverbuild.py +0 -0
  17. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/drivers/__init__.py +0 -0
  18. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/drivers/cpp.py +0 -0
  19. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/drivers/fortran.py +0 -0
  20. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/__init__.py +0 -0
  21. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/cmake_gen.py +0 -0
  22. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/docker_gen.py +0 -0
  23. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/error_gen.py +0 -0
  24. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/f2py_gen.py +0 -0
  25. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/gateway_gen.py +0 -0
  26. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/golden_gen.py +0 -0
  27. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/k8s_gen.py +0 -0
  28. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/mcp_gen.py +0 -0
  29. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/middleware_gen.py +0 -0
  30. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/pybind_gen.py +0 -0
  31. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/pyproject_gen.py +0 -0
  32. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/python_pkg_gen.py +0 -0
  33. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/test_gen.py +0 -0
  34. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/golden.py +0 -0
  35. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/invariants.py +0 -0
  36. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/ir.py +0 -0
  37. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/lattice.py +0 -0
  38. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/locking.py +0 -0
  39. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/oracle.py +0 -0
  40. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/__init__.py +0 -0
  41. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/cpp.py +0 -0
  42. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/cpp_ast.py +0 -0
  43. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/cpp_regex.py +0 -0
  44. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fixed_form.py +0 -0
  45. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fortran.py +0 -0
  46. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fortran_fparser.py +0 -0
  47. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fortran_regex.py +0 -0
  48. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/preprocess.py +0 -0
  49. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/structural_invariants.py +0 -0
  50. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/suggest.py +0 -0
  51. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/templates/__init__.py +0 -0
  52. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/templates/golden_test_template.py +0 -0
  53. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/wire.py +0 -0
  54. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/dependency_links.txt +0 -0
  55. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/entry_points.txt +0 -0
  56. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/requires.txt +0 -0
  57. {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/top_level.txt +0 -0
  58. {nativegate-0.1.1 → nativegate-0.1.3}/setup.cfg +0 -0
  59. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_buildinfo.py +0 -0
  60. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cli_serve_verify.py +0 -0
  61. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cli_suggest.py +0 -0
  62. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cli_verify_aggregate.py +0 -0
  63. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cmake_gen.py +0 -0
  64. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cpp_ast.py +0 -0
  65. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cpp_driver.py +0 -0
  66. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cpp_pipeline.py +0 -0
  67. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_declared_invariants.py +0 -0
  68. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_derived_types.py +0 -0
  69. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_docker_gen.py +0 -0
  70. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_driverbuild.py +0 -0
  71. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_endpoint_docstrings.py +0 -0
  72. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fixed_form.py +0 -0
  73. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_doc_comments.py +0 -0
  74. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_driver.py +0 -0
  75. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_fparser.py +0 -0
  76. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_libraries.py +0 -0
  77. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_pipeline.py +0 -0
  78. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_gateway_gen.py +0 -0
  79. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_generated_python_is_valid.py +0 -0
  80. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_golden.py +0 -0
  81. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_invariants.py +0 -0
  82. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_ir_contract.py +0 -0
  83. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_k8s_gen.py +0 -0
  84. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_lattice.py +0 -0
  85. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_legacy_cpp.py +0 -0
  86. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_legacy_fortran.py +0 -0
  87. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_locking.py +0 -0
  88. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_mcp_gen.py +0 -0
  89. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_middleware_gen.py +0 -0
  90. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_multifile_cpp.py +0 -0
  91. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_native_oracle.py +0 -0
  92. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_native_state_contract.py +0 -0
  93. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_oracle.py +0 -0
  94. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_preprocess_and_config.py +0 -0
  95. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_preprocessed_fortran.py +0 -0
  96. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_pybind_gen.py +0 -0
  97. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_pyproject_gen.py +0 -0
  98. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_quickstart.py +0 -0
  99. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_scalar_ref.py +0 -0
  100. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_service_gen.py +0 -0
  101. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_shared_libraries.py +0 -0
  102. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_structural_invariants.py +0 -0
  103. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_suggest.py +0 -0
  104. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_verification_config.py +0 -0
  105. {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_wire.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nativegate
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -60,6 +60,7 @@ from .ir import (
60
60
  IRSchemaError,
61
61
  ModuleIR,
62
62
  NativeTypeError,
63
+ is_valid_python_name,
63
64
  module_from_dict,
64
65
  module_to_dict,
65
66
  validate as validate_ir,
@@ -68,6 +69,7 @@ from .parsers import cpp as cpp_parser
68
69
  from .parsers.cpp_ast import ClangOptions
69
70
  from .parsers import fixed_form
70
71
  from .parsers import fortran as fortran_parser
72
+ from .parsers.dialect import apply_dialect, is_dialect_marked, resolve_dialect
71
73
 
72
74
  SERVICES_DIR = "services"
73
75
 
@@ -243,7 +245,15 @@ def create_service(name: str, language: str, force: bool) -> None:
243
245
  help="Delete and recreate services/<name> if it already exists.",
244
246
  )
245
247
  @click.option("--build/--no-build", default=False, help="Also compile the extension after generating.")
246
- 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:
247
257
  """One-shot: scaffold a service from a single C++/Fortran file, expose
248
258
  everything found in it, and generate the Python package — no manual
249
259
  nativegate.yaml editing.
@@ -309,10 +319,51 @@ def quickstart(source: Path, name: str | None, force: bool, build: bool) -> None
309
319
  f"{service_dir / 'native'} and re-run `ngate generate {service_name}`."
310
320
  )
311
321
  else:
312
- 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
313
356
  if not routines:
314
357
  tracker.fail("Copy native source")
315
- 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
+ )
316
367
  config.expose.functions = routines
317
368
  config.save(service_dir)
318
369
  tracker.log(f"Exposing Fortran routines: {', '.join(routines)}")
@@ -1241,6 +1292,22 @@ def clean(name: str) -> None:
1241
1292
  def _scaffold_service(name: str, language: str, force: bool) -> Path:
1242
1293
  import shutil
1243
1294
 
1295
+ # Reject before anything is written: the name becomes a Python package
1296
+ # (services/<name>/python/<name>/), so `specfun-gamma` would fail at
1297
+ # generate time with `from ._native.specfun-gamma import ...` — invalid
1298
+ # syntax caught by the generated-file compile() gate, three commands
1299
+ # and a wasted scaffold after the user typed the name. Saying so here
1300
+ # costs nothing (DEFECTS D9, found on first contact with netlib
1301
+ # specfun, where quickstart defaults the name to the file stem and
1302
+ # `specfun-gamma` is the obvious thing to type).
1303
+ if not is_valid_python_name(name):
1304
+ raise click.ClickException(
1305
+ f"Service name '{name}' is not usable: it becomes a Python package, "
1306
+ "so it must be a valid identifier (letters, digits, underscores, "
1307
+ "not starting with a digit, not a Python keyword). "
1308
+ "Use underscores instead of dashes."
1309
+ )
1310
+
1244
1311
  service_dir = Path(SERVICES_DIR) / name
1245
1312
  if service_dir.exists():
1246
1313
  if not force:
@@ -1702,6 +1769,35 @@ def _generate_fortran_service(service_dir: Path, config: ServiceConfig) -> None:
1702
1769
  if not sources:
1703
1770
  raise click.ClickException(f"No Fortran sources found under {service_dir / 'native'}.")
1704
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
+
1705
1801
  include_paths = [Path(p) for p in config.include_paths]
1706
1802
  for p in include_paths:
1707
1803
  if not p.is_dir():
@@ -1893,13 +1989,16 @@ def _generate_fortran_service(service_dir: Path, config: ServiceConfig) -> None:
1893
1989
  + kind_param_sources
1894
1990
  + shim_only_sources
1895
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)
1896
1995
  # A library source is always copied under the service, even when it needs
1897
1996
  # no rewriting: it lives outside native/, so referencing it as
1898
1997
  # `native/<name>` would name a file that is not there — and the Docker
1899
1998
  # build context is the service directory, so a path outside it could not be
1900
1999
  # COPYed in anyway.
1901
2000
  library_set = set(library_sources)
1902
- if rewritten or library_sources or preprocessed_set:
2001
+ if rewritten_or_copied:
1903
2002
  expanded_dir = service_dir / "native" / "_expanded"
1904
2003
  expanded_dir.mkdir(parents=True, exist_ok=True)
1905
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.
@@ -819,6 +827,7 @@ class ServiceConfig:
819
827
  ),
820
828
  libraries=list(data.get("libraries") or []),
821
829
  include_paths=list(data.get("include_paths") or []),
830
+ dialect=resolve_dialect(data.get("dialect")),
822
831
  api=_load_api(data.get("api"), config_path),
823
832
  fortran_defines=list((data.get("fortran") or {}).get("defines") or []),
824
833
  verification=_load_verification(data, config_path, known_functions),
@@ -853,6 +862,8 @@ class ServiceConfig:
853
862
  data["libraries"] = self.libraries
854
863
  if self.include_paths:
855
864
  data["include_paths"] = self.include_paths
865
+ if self.dialect:
866
+ data["dialect"] = self.dialect
856
867
  # Only written when it differs from the default, so an existing
857
868
  # nativegate.yaml does not grow a key that says nothing.
858
869
  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.1
3
+ Version: 0.1.3
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
@@ -61,6 +62,7 @@ tests/test_cpp_driver.py
61
62
  tests/test_cpp_pipeline.py
62
63
  tests/test_declared_invariants.py
63
64
  tests/test_derived_types.py
65
+ tests/test_dialect.py
64
66
  tests/test_docker_gen.py
65
67
  tests/test_driverbuild.py
66
68
  tests/test_endpoint_docstrings.py
@@ -93,6 +95,7 @@ tests/test_pyproject_gen.py
93
95
  tests/test_quickstart.py
94
96
  tests/test_scalar_ref.py
95
97
  tests/test_service_gen.py
98
+ tests/test_service_name_validation.py
96
99
  tests/test_shared_libraries.py
97
100
  tests/test_structural_invariants.py
98
101
  tests/test_suggest.py
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
13
13
 
14
14
  [project]
15
15
  name = "nativegate"
16
- version = "0.1.1"
16
+ version = "0.1.3"
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,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
@@ -0,0 +1,47 @@
1
+ """D9 regression: dash-named services are rejected at scaffold time, not
2
+ at generate time with invalid generated Python.
3
+
4
+ First contact with netlib specfun (2026-09-23): `ngate quickstart gamma.f
5
+ --name specfun-gamma` scaffolded cleanly, generated
6
+ `from ._native.specfun-gamma import ...` — invalid syntax — and only
7
+ nativegate's generated-file compile() gate refused to write it, three
8
+ commands after the moment the user should have been told.
9
+ """
10
+
11
+ import pytest
12
+ from click.testing import CliRunner
13
+
14
+ from nativegate import cli
15
+
16
+
17
+ @pytest.fixture()
18
+ def scratch(tmp_path, monkeypatch):
19
+ monkeypatch.chdir(tmp_path)
20
+ return tmp_path
21
+
22
+
23
+ @pytest.mark.parametrize("bad", ["specfun-gamma", "2lib", "class"])
24
+ def test_create_service_rejects_non_identifier_names(scratch, bad):
25
+ result = CliRunner().invoke(cli.main, ["create-service", bad, "--language", "fortran"])
26
+ assert result.exit_code != 0
27
+ assert "Python package" in result.output
28
+ # nothing may have been written
29
+ assert not (scratch / "services").exists()
30
+
31
+
32
+ def test_quickstart_rejects_dash_named_service_and_explains(tmp_path, monkeypatch):
33
+ monkeypatch.chdir(tmp_path)
34
+ source = tmp_path / "gamma.f"
35
+ source.write_text(" DOUBLE PRECISION FUNCTION DGAMMA(X)\n END\n")
36
+ result = CliRunner().invoke(
37
+ cli.main, ["quickstart", str(source), "--name", "specfun-gamma"]
38
+ )
39
+ assert result.exit_code != 0
40
+ assert "--name specfun_gamma" not in result.output
41
+
42
+
43
+ def test_valid_underscored_name_still_scaffolds(scratch):
44
+ result = CliRunner().invoke(
45
+ cli.main, ["create-service", "specfun_gamma", "--language", "fortran"]
46
+ )
47
+ assert result.exit_code == 0, result.output
File without changes
File without changes
File without changes