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.
- {nativegate-0.1.1 → nativegate-0.1.3}/PKG-INFO +1 -1
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/cli.py +103 -4
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/config.py +11 -0
- nativegate-0.1.3/nativegate/parsers/dialect.py +98 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/PKG-INFO +1 -1
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/SOURCES.txt +3 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/pyproject.toml +1 -1
- nativegate-0.1.3/tests/test_dialect.py +184 -0
- nativegate-0.1.3/tests/test_service_name_validation.py +47 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/README.md +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/__init__.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/__main__.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/buildinfo.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/declared_invariants.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/discovery.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/driverbuild.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/drivers/__init__.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/drivers/cpp.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/drivers/fortran.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/__init__.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/cmake_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/docker_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/error_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/f2py_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/gateway_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/golden_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/k8s_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/mcp_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/middleware_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/pybind_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/pyproject_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/python_pkg_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/generators/test_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/golden.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/invariants.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/ir.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/lattice.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/locking.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/oracle.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/__init__.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/cpp.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/cpp_ast.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/cpp_regex.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fixed_form.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fortran.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fortran_fparser.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/parsers/fortran_regex.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/preprocess.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/structural_invariants.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/suggest.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/templates/__init__.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/templates/golden_test_template.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate/wire.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/dependency_links.txt +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/entry_points.txt +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/requires.txt +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/nativegate.egg-info/top_level.txt +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/setup.cfg +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_buildinfo.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cli_serve_verify.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cli_suggest.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cli_verify_aggregate.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cmake_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cpp_ast.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cpp_driver.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_cpp_pipeline.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_declared_invariants.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_derived_types.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_docker_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_driverbuild.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_endpoint_docstrings.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fixed_form.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_doc_comments.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_driver.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_fparser.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_libraries.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_fortran_pipeline.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_gateway_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_generated_python_is_valid.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_golden.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_invariants.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_ir_contract.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_k8s_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_lattice.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_legacy_cpp.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_legacy_fortran.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_locking.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_mcp_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_middleware_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_multifile_cpp.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_native_oracle.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_native_state_contract.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_oracle.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_preprocess_and_config.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_preprocessed_fortran.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_pybind_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_pyproject_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_quickstart.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_scalar_ref.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_service_gen.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_shared_libraries.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_structural_invariants.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_suggest.py +0 -0
- {nativegate-0.1.1 → nativegate-0.1.3}/tests/test_verification_config.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|