nativegate 0.1.6__tar.gz → 0.1.8__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.6 → nativegate-0.1.8}/PKG-INFO +1 -1
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/cli.py +28 -1
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/fortran_fparser.py +59 -6
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/fortran_regex.py +75 -5
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate.egg-info/PKG-INFO +1 -1
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate.egg-info/SOURCES.txt +2 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/pyproject.toml +1 -1
- nativegate-0.1.8/tests/test_external_callbacks.py +79 -0
- nativegate-0.1.8/tests/test_inspect_dialect.py +48 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/README.md +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/__init__.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/__main__.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/buildinfo.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/config.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/declared_invariants.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/discovery.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/driverbuild.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/drivers/__init__.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/drivers/cpp.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/drivers/fortran.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/__init__.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/cmake_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/docker_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/error_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/f2py_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/gateway_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/golden_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/k8s_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/mcp_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/middleware_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/pybind_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/pyproject_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/python_pkg_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/generators/test_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/golden.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/invariants.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/ir.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/lattice.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/locking.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/oracle.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/__init__.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/cpp.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/cpp_ast.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/cpp_regex.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/dialect.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/fixed_form.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/parsers/fortran.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/preprocess.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/structural_invariants.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/suggest.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/templates/__init__.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/templates/golden_test_template.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate/wire.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate.egg-info/dependency_links.txt +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate.egg-info/entry_points.txt +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate.egg-info/requires.txt +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/nativegate.egg-info/top_level.txt +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/setup.cfg +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_buildinfo.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cli_serve_verify.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cli_suggest.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cli_verify_aggregate.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cmake_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cmake_missing_input.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_config_unknown_keys.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cpp_ast.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cpp_driver.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_cpp_pipeline.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_declared_invariants.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_derived_types.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_dialect.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_docker_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_driverbuild.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_endpoint_docstrings.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_fixed_form.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_fortran_doc_comments.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_fortran_driver.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_fortran_fparser.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_fortran_libraries.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_fortran_pipeline.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_gateway_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_generated_python_is_valid.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_golden.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_invariants.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_ir_contract.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_k8s_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_lattice.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_legacy_cpp.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_legacy_fortran.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_locking.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_mcp_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_middleware_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_multifile_cpp.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_native_oracle.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_native_state_contract.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_oracle.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_preprocess_and_config.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_preprocessed_fortran.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_pybind_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_pyproject_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_quickstart.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_scalar_ref.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_service_gen.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_service_name_validation.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_shared_libraries.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_structural_invariants.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_suggest.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/tests/test_verification_config.py +0 -0
- {nativegate-0.1.6 → nativegate-0.1.8}/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.8
|
|
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
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import shutil
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
+
import tempfile
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
import click
|
|
@@ -513,12 +514,21 @@ def suggest(path: Path, parser: str, includes: tuple[str, ...], show_all: bool,
|
|
|
513
514
|
show_default=True,
|
|
514
515
|
help="C++ only: language standard for the AST parse.",
|
|
515
516
|
)
|
|
517
|
+
@click.option(
|
|
518
|
+
"--dialect",
|
|
519
|
+
default="",
|
|
520
|
+
help="Fortran only: resolve netlib CS/CD dual-dialect marking "
|
|
521
|
+
"('cs' or 'cd') in a temporary copy before parsing. Mirrors "
|
|
522
|
+
"nativegate.yaml's `dialect:` so `ngate inspect` answers the same "
|
|
523
|
+
"questions about a marked source that `generate` will.",
|
|
524
|
+
)
|
|
516
525
|
def inspect(
|
|
517
526
|
path: Path,
|
|
518
527
|
functions: tuple[str, ...],
|
|
519
528
|
parser: str,
|
|
520
529
|
includes: tuple[str, ...],
|
|
521
530
|
std: str,
|
|
531
|
+
dialect: str,
|
|
522
532
|
) -> None:
|
|
523
533
|
"""Parse a native source file and print the resulting IR."""
|
|
524
534
|
lang = detect_language(path)
|
|
@@ -539,7 +549,24 @@ def inspect(
|
|
|
539
549
|
"Fortran sources need at least one --function <name> to target "
|
|
540
550
|
"(large legacy files are parsed one routine at a time)."
|
|
541
551
|
)
|
|
542
|
-
|
|
552
|
+
inspect_path = path
|
|
553
|
+
chosen = resolve_dialect(dialect)
|
|
554
|
+
if chosen and is_dialect_marked(path.read_text()):
|
|
555
|
+
inspect_path = Path(tempfile.mkstemp(suffix=path.suffix)[1])
|
|
556
|
+
inspect_path.write_text(apply_dialect(read_source(path), chosen))
|
|
557
|
+
try:
|
|
558
|
+
module = fortran_parser.parse_source(
|
|
559
|
+
inspect_path, ExposeConfig(functions=list(functions))
|
|
560
|
+
)
|
|
561
|
+
except ValueError as exc:
|
|
562
|
+
hint = ""
|
|
563
|
+
if not chosen and is_dialect_marked(path.read_text()):
|
|
564
|
+
hint = (
|
|
565
|
+
"\nThe file looks dialect-marked (CS/CD prefixes on both "
|
|
566
|
+
"copies of every line). Pass --dialect cs or --dialect cd."
|
|
567
|
+
)
|
|
568
|
+
raise click.ClickException(f"{exc}{hint}") from exc
|
|
569
|
+
|
|
543
570
|
else:
|
|
544
571
|
raise click.ClickException(f"Unrecognized native source: {path} (detected language: {lang}).")
|
|
545
572
|
|
|
@@ -454,6 +454,13 @@ class _Declarations:
|
|
|
454
454
|
derived_intents: dict[str, str] = field(default_factory=dict)
|
|
455
455
|
# Names given a shape by a standalone DIMENSION statement.
|
|
456
456
|
dimensioned: set[str] = field(default_factory=set)
|
|
457
|
+
# Names declared EXTERNAL — Fortran procedure dummies (callbacks). f2py
|
|
458
|
+
# binds these only with per-argument callback directives, which nativegate
|
|
459
|
+
# does not generate; typing one by its IMPLICIT fallback (`real`, because
|
|
460
|
+
# 'f' is not in the I-N band) produced a binding that builds, imports,
|
|
461
|
+
# passes its smoke test and hands Fortran garbage at call time. Recorded
|
|
462
|
+
# first contact: netlib quadpack's QNG/QAGSE (DEFECTS D14).
|
|
463
|
+
external: set[str] = field(default_factory=set)
|
|
457
464
|
|
|
458
465
|
|
|
459
466
|
def _parse_declarations(spec_part) -> _Declarations:
|
|
@@ -468,6 +475,17 @@ def _parse_declarations(spec_part) -> _Declarations:
|
|
|
468
475
|
for item in stmt.items[0]:
|
|
469
476
|
result.dimensioned.add(_name_of(item[0]).lower())
|
|
470
477
|
|
|
478
|
+
# EXTERNAL names are Fortran procedure dummies — callbacks. See the
|
|
479
|
+
# `external` field note above for why typing them is a silently wrong
|
|
480
|
+
# answer, and DEFECTS D14 for the netlib quadpack contact. External_Name
|
|
481
|
+
# nodes never appear as standalone tree entries, so read the names off
|
|
482
|
+
# External_Stmt.items: item 0 is the keyword, the last is the name list
|
|
483
|
+
# (the arity lines up across the fixed- and free-form grammars).
|
|
484
|
+
for stmt in _walk(spec_part, _F.External_Stmt):
|
|
485
|
+
name_list = stmt.items[-1]
|
|
486
|
+
for item in getattr(name_list, "items", ()):
|
|
487
|
+
result.external.add(_name_of(item).lower())
|
|
488
|
+
|
|
471
489
|
for decl in _walk(spec_part, _F.Type_Declaration_Stmt):
|
|
472
490
|
type_spec = decl.items[0]
|
|
473
491
|
if _is_derived(type_spec):
|
|
@@ -698,17 +716,30 @@ def _resolve_parameters(
|
|
|
698
716
|
declared: dict[str, Parameter],
|
|
699
717
|
unbindable: dict[str, str],
|
|
700
718
|
implicit_map: dict[str, str],
|
|
719
|
+
external_names: set[str] = frozenset(),
|
|
701
720
|
) -> list[Parameter]:
|
|
702
721
|
"""Type every dummy argument, or refuse the routine.
|
|
703
722
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
723
|
+
Four sources in decreasing authority: EXTERNAL declarations (refuse the
|
|
724
|
+
routine — callbacks have their own semantic and no generated binding),
|
|
725
|
+
an explicit declaration, a recognised-but-unbindable declaration
|
|
726
|
+
(refuse), and IMPLICIT typing. There is deliberately no fifth "assume
|
|
727
|
+
float" step — the reason the reasons below exist at all.
|
|
708
728
|
"""
|
|
709
729
|
parameters: list[Parameter] = []
|
|
710
730
|
for param_name in param_names:
|
|
711
731
|
key = param_name.lower()
|
|
732
|
+
# EXTERNAL check ahead of every type lookup: a name in the external
|
|
733
|
+
# list is a procedure dummy, whatever it implicitly types as. 'f' is
|
|
734
|
+
# not in the I-N band, so the fallback it used to hit was real/float.
|
|
735
|
+
if key in external_names:
|
|
736
|
+
raise _RoutineSkipped(
|
|
737
|
+
f"argument '{param_name}' is declared EXTERNAL (a Fortran "
|
|
738
|
+
"procedure/callback). f2py binds callbacks only through per-"
|
|
739
|
+
"argument f2py directives, which the generator does not emit "
|
|
740
|
+
"yet — this routine is skipped as a whole rather than bound "
|
|
741
|
+
"with the callback mis-typed as a scalar."
|
|
742
|
+
)
|
|
712
743
|
if key in declared:
|
|
713
744
|
parameters.append(declared[key])
|
|
714
745
|
continue
|
|
@@ -824,11 +855,11 @@ def _free_form_routine(
|
|
|
824
855
|
parameters.extend(flat_by_param[param])
|
|
825
856
|
else:
|
|
826
857
|
parameters.extend(
|
|
827
|
-
_resolve_parameters([param], declared, unbindable, implicit_map)
|
|
858
|
+
_resolve_parameters([param], declared, unbindable, implicit_map, decls.external)
|
|
828
859
|
)
|
|
829
860
|
else:
|
|
830
861
|
parameters = _resolve_parameters(
|
|
831
|
-
param_names, declared, unbindable, implicit_map
|
|
862
|
+
param_names, declared, unbindable, implicit_map, decls.external
|
|
832
863
|
)
|
|
833
864
|
|
|
834
865
|
if is_subroutine:
|
|
@@ -987,8 +1018,26 @@ def _parse_fixed_form(path: Path, expose: ExposeConfig, include_paths: list[Path
|
|
|
987
1018
|
)
|
|
988
1019
|
|
|
989
1020
|
parameters = []
|
|
1021
|
+
external_refused = False
|
|
990
1022
|
for param_name in param_names:
|
|
991
1023
|
key = param_name.lower()
|
|
1024
|
+
# Ahead of every type lookup: an EXTERNAL name is a procedure
|
|
1025
|
+
# dummy whatever it implicitly types as (DEFECTS D14 — netlib
|
|
1026
|
+
# quadpack QNG/QAGSE).
|
|
1027
|
+
if key in decls.external:
|
|
1028
|
+
module.skipped.append(
|
|
1029
|
+
SkippedSymbol(
|
|
1030
|
+
name,
|
|
1031
|
+
f"argument '{param_name}' is declared EXTERNAL (a "
|
|
1032
|
+
"Fortran procedure/callback). f2py binds callbacks "
|
|
1033
|
+
"only through per-argument f2py directives, which "
|
|
1034
|
+
"the generator does not emit yet — skipped as a "
|
|
1035
|
+
"whole rather than bound with the callback "
|
|
1036
|
+
"mis-typed as a scalar.",
|
|
1037
|
+
)
|
|
1038
|
+
)
|
|
1039
|
+
external_refused = True
|
|
1040
|
+
break
|
|
992
1041
|
declaration = decls.declared.get(key)
|
|
993
1042
|
if declaration is not None:
|
|
994
1043
|
py_type = declaration.type
|
|
@@ -1068,6 +1117,10 @@ def _parse_fixed_form(path: Path, expose: ExposeConfig, include_paths: list[Path
|
|
|
1068
1117
|
)
|
|
1069
1118
|
returns = map_fortran_type(base) if base else "float"
|
|
1070
1119
|
|
|
1120
|
+
if external_refused:
|
|
1121
|
+
# Recorded in module.skipped; nothing downstream may wrap it.
|
|
1122
|
+
continue
|
|
1123
|
+
|
|
1071
1124
|
module.functions.append(
|
|
1072
1125
|
FunctionDef(
|
|
1073
1126
|
name=name,
|
|
@@ -299,6 +299,29 @@ def _parse_derived_declarations(body: str) -> dict[str, str]:
|
|
|
299
299
|
return unbindable
|
|
300
300
|
|
|
301
301
|
|
|
302
|
+
# EXTERNAL declarations name Fortran procedure dummies — callbacks. Stands
|
|
303
|
+
# in the fixed-form `external f` shape AND the free-form `external :: f, g`
|
|
304
|
+
# one. Collected so the routine that takes one is skipped with a reason
|
|
305
|
+
# (both readers, identical text): typing it through the IMPLICIT fallback —
|
|
306
|
+
# 'f' is real, since 'f' sits outside the I-N integer band — produced a
|
|
307
|
+
# binding that builds, imports, smokes green and hands Fortran garbage at
|
|
308
|
+
# call time. First contact: netlib quadpack QNG/QAGSE (DEFECTS D14).
|
|
309
|
+
_EXTERNAL_RE = re.compile(
|
|
310
|
+
r"^[ \t]*external[ \t]*(?::[ \t]*)?([a-z][a-z0-9_, \t]*)$",
|
|
311
|
+
re.IGNORECASE | re.MULTILINE,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _external_declared_names(body: str) -> set[str]:
|
|
316
|
+
out: set[str] = set()
|
|
317
|
+
for match in _EXTERNAL_RE.finditer(body):
|
|
318
|
+
for raw in match.group(1).split(","):
|
|
319
|
+
name = raw.strip().lower()
|
|
320
|
+
if name:
|
|
321
|
+
out.add(name)
|
|
322
|
+
return out
|
|
323
|
+
|
|
324
|
+
|
|
302
325
|
def _parse_declarations(body: str) -> dict[str, Parameter]:
|
|
303
326
|
"""Map declared variable name -> Parameter, for every `type, attrs :: names` line in body."""
|
|
304
327
|
declared: dict[str, Parameter] = {}
|
|
@@ -336,19 +359,34 @@ def _resolve_parameters(
|
|
|
336
359
|
declared: dict[str, Parameter],
|
|
337
360
|
unbindable: dict[str, str],
|
|
338
361
|
implicit_map: dict[str, str],
|
|
362
|
+
external_names=None,
|
|
339
363
|
) -> list[Parameter]:
|
|
340
364
|
"""Type every dummy argument, or refuse the routine.
|
|
341
365
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
366
|
+
Four sources in decreasing authority: EXTERNAL declarations (refuse the
|
|
367
|
+
routine — a callback typed via the IMPLICIT fallback is a buildable,
|
|
368
|
+
importable, silently wrong binding; see DEFECTS D14 / netlib quadpack),
|
|
369
|
+
an explicit declaration, a recognised-but-unbindable declaration
|
|
370
|
+
(refuse), and IMPLICIT typing. There is deliberately no fifth "assume
|
|
371
|
+
float" step.
|
|
345
372
|
"""
|
|
373
|
+
external_names = external_names if external_names is not None else set()
|
|
346
374
|
raw_params = match.group("params") or ""
|
|
347
375
|
param_names = [p.strip() for p in raw_params.split(",") if p.strip()]
|
|
348
376
|
|
|
349
377
|
parameters: list[Parameter] = []
|
|
350
378
|
for param_name in param_names:
|
|
351
379
|
key = param_name.lower()
|
|
380
|
+
# Ahead of every type lookup: an EXTERNAL name is a procedure dummy
|
|
381
|
+
# whatever it implicitly types as — see the note on _EXTERNAL_RE.
|
|
382
|
+
if key in (external_names or ()):
|
|
383
|
+
raise _RoutineSkipped(
|
|
384
|
+
f"argument '{param_name}' is declared EXTERNAL (a Fortran "
|
|
385
|
+
"procedure/callback). f2py binds callbacks only through per-"
|
|
386
|
+
"argument f2py directives, which the generator does not emit "
|
|
387
|
+
"yet — this routine is skipped as a whole rather than bound "
|
|
388
|
+
"with the callback mis-typed as a scalar."
|
|
389
|
+
)
|
|
352
390
|
if key in declared:
|
|
353
391
|
parameters.append(declared[key])
|
|
354
392
|
continue
|
|
@@ -431,7 +469,9 @@ def _extract_function(
|
|
|
431
469
|
declared = _parse_declarations(body)
|
|
432
470
|
unbindable = _parse_derived_declarations(body)
|
|
433
471
|
|
|
434
|
-
parameters = _resolve_parameters(
|
|
472
|
+
parameters = _resolve_parameters(
|
|
473
|
+
match, declared, unbindable, implicit_map, _external_declared_names(body)
|
|
474
|
+
)
|
|
435
475
|
|
|
436
476
|
result_name = (match.group("result") or name).lower()
|
|
437
477
|
result_param = declared.get(result_name)
|
|
@@ -476,7 +516,9 @@ def _extract_subroutine(
|
|
|
476
516
|
declared = _parse_declarations(body)
|
|
477
517
|
unbindable = _parse_derived_declarations(body)
|
|
478
518
|
|
|
479
|
-
parameters = _resolve_parameters(
|
|
519
|
+
parameters = _resolve_parameters(
|
|
520
|
+
match, declared, unbindable, implicit_map, _external_declared_names(body)
|
|
521
|
+
)
|
|
480
522
|
|
|
481
523
|
enclosing = _enclosing_fortran_module(source, match.start())
|
|
482
524
|
fn = FunctionDef(
|
|
@@ -537,9 +579,32 @@ def _parse_fixed_form_source(
|
|
|
537
579
|
# `STEP(DTIN, DTOUT, ICONV)` would return None.
|
|
538
580
|
intents = fixed_form.infer_intents(routine, normalized)
|
|
539
581
|
|
|
582
|
+
# The fixed-form half has its own declaration scan (`find_routine` +
|
|
583
|
+
# `declared_types`), which never consulted the free-form extractors
|
|
584
|
+
# — so an `external f` (a Fortran procedure dummy) typed straight
|
|
585
|
+
# through the IMPLICIT fallback to `real`/float and produced a
|
|
586
|
+
# binding that builds, imports, passes its smoke test and hands
|
|
587
|
+
# Fortran garbage at call time. Found on netlib quadpack QNG/QAGSE
|
|
588
|
+
# (DEFECTS D14); refusal is the interpretation both backends share.
|
|
589
|
+
external_names = _external_declared_names(routine["body"])
|
|
540
590
|
parameters = []
|
|
541
591
|
for param_name in routine["params"]:
|
|
542
592
|
key = param_name.lower()
|
|
593
|
+
# Ahead of every type lookup: an EXTERNAL name is a procedure
|
|
594
|
+
# dummy whatever it implicitly types as — see the note above.
|
|
595
|
+
if key in external_names:
|
|
596
|
+
module.skipped.append(
|
|
597
|
+
SkippedSymbol(
|
|
598
|
+
name,
|
|
599
|
+
f"argument '{param_name}' is declared EXTERNAL (a "
|
|
600
|
+
"Fortran procedure/callback). f2py binds callbacks "
|
|
601
|
+
"only through per-argument f2py directives, which "
|
|
602
|
+
"the generator does not emit yet — skipped as a "
|
|
603
|
+
"whole rather than bound with the callback "
|
|
604
|
+
"mis-typed as a scalar.",
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
break
|
|
543
608
|
base_type = routine["declared_types"].get(key) or fixed_form.implicit_type_of(
|
|
544
609
|
param_name, implicit_map
|
|
545
610
|
)
|
|
@@ -602,6 +667,11 @@ def _parse_fixed_form_source(
|
|
|
602
667
|
)
|
|
603
668
|
returns = map_fortran_type(base) if base else "float"
|
|
604
669
|
|
|
670
|
+
# A routine whose whole binding was refused (external callback) has
|
|
671
|
+
# been recorded in module.skipped; nothing downstream may wrap it.
|
|
672
|
+
if any(s.name == name for s in module.skipped):
|
|
673
|
+
continue
|
|
674
|
+
|
|
605
675
|
module.functions.append(
|
|
606
676
|
FunctionDef(
|
|
607
677
|
name=name,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nativegate
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
@@ -68,6 +68,7 @@ tests/test_dialect.py
|
|
|
68
68
|
tests/test_docker_gen.py
|
|
69
69
|
tests/test_driverbuild.py
|
|
70
70
|
tests/test_endpoint_docstrings.py
|
|
71
|
+
tests/test_external_callbacks.py
|
|
71
72
|
tests/test_fixed_form.py
|
|
72
73
|
tests/test_fortran_doc_comments.py
|
|
73
74
|
tests/test_fortran_driver.py
|
|
@@ -77,6 +78,7 @@ tests/test_fortran_pipeline.py
|
|
|
77
78
|
tests/test_gateway_gen.py
|
|
78
79
|
tests/test_generated_python_is_valid.py
|
|
79
80
|
tests/test_golden.py
|
|
81
|
+
tests/test_inspect_dialect.py
|
|
80
82
|
tests/test_invariants.py
|
|
81
83
|
tests/test_ir_contract.py
|
|
82
84
|
tests/test_k8s_gen.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.8"
|
|
17
17
|
description = "Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""D14: `external` dummies are callbacks — refusing to type them.
|
|
2
|
+
|
|
3
|
+
First contact: netlib quadpack QAGSE (f2py itself refuses — 'external f
|
|
4
|
+
is not in lcb_map[]'). The regex reader's IMPLICIT fallback typed 'f' as
|
|
5
|
+
`real` -> float: a binding that builds, imports, passes its smoke test
|
|
6
|
+
and hands the Fortran side garbage at call time. Both backends now
|
|
7
|
+
refuse the routine as a whole with the reason; ModuleIR.skipped carries
|
|
8
|
+
it. NOTE: the synthetic decks are written with hard six-space margins —
|
|
9
|
+
textwrap.dedent strips the shared indent and the files stop being
|
|
10
|
+
fixed-form.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from nativegate.config import ExposeConfig
|
|
16
|
+
from nativegate.parsers import fortran as fortran_parser
|
|
17
|
+
|
|
18
|
+
HEADER_SUB = (
|
|
19
|
+
" SUBROUTINE QNG(F, A, B)\n"
|
|
20
|
+
" EXTERNAL F\n"
|
|
21
|
+
" REAL A, B\n"
|
|
22
|
+
" END\n"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
HEADER_FREE = (
|
|
26
|
+
"subroutine qng(f, a, b)\n"
|
|
27
|
+
" external f\n"
|
|
28
|
+
" real a, b\n"
|
|
29
|
+
"end subroutine qng\n"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
HEADER_PLAIN = (
|
|
33
|
+
" SUBROUTINE ASSOC(A, B)\n"
|
|
34
|
+
" REAL A, B\n"
|
|
35
|
+
" A = B\n"
|
|
36
|
+
" RETURN\n"
|
|
37
|
+
" END\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_regex_backend_reports_external_dummy_as_skip(tmp_path):
|
|
42
|
+
src = tmp_path / "qng.f"
|
|
43
|
+
src.write_text(HEADER_SUB)
|
|
44
|
+
module = fortran_parser.parse_source(
|
|
45
|
+
src, ExposeConfig(functions=["QNG"]), backend="regex"
|
|
46
|
+
)
|
|
47
|
+
assert module.functions == []
|
|
48
|
+
assert len(module.skipped) == 1
|
|
49
|
+
assert module.skipped[0].name == "QNG"
|
|
50
|
+
assert "EXTERNAL" in module.skipped[0].reason
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_fparser_backend_reports_external_dummy_as_skip(tmp_path):
|
|
54
|
+
src = tmp_path / "qng.f"
|
|
55
|
+
src.write_text(HEADER_SUB)
|
|
56
|
+
module = fortran_parser.parse_source(
|
|
57
|
+
src, ExposeConfig(functions=["QNG"]), backend="fparser2"
|
|
58
|
+
)
|
|
59
|
+
assert module.functions == []
|
|
60
|
+
assert "EXTERNAL" in module.skipped[0].reason
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_free_form_backend_also_refuses_callbacks(tmp_path):
|
|
64
|
+
src = tmp_path / "qng.f90"
|
|
65
|
+
src.write_text(HEADER_FREE)
|
|
66
|
+
module = fortran_parser.parse_source(
|
|
67
|
+
src, ExposeConfig(functions=["qng"]), backend="regex"
|
|
68
|
+
)
|
|
69
|
+
assert module.functions == []
|
|
70
|
+
assert len(module.skipped) == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_routines_without_callbacks_still_bind(tmp_path):
|
|
74
|
+
src = tmp_path / "g.f"
|
|
75
|
+
src.write_text(HEADER_PLAIN)
|
|
76
|
+
module = fortran_parser.parse_source(
|
|
77
|
+
src, ExposeConfig(functions=["ASSOC"]), backend="regex"
|
|
78
|
+
)
|
|
79
|
+
assert [p.name.lower() for p in module.functions[0].parameters] == ["a", "b"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""`inspect` speaks dialect: cd too — parity with quickstart/generate.
|
|
2
|
+
|
|
3
|
+
The cli-reference/fortran-guide contract: a netlib dual-dialect file
|
|
4
|
+
answers the same questions through inspect with a flag that generate
|
|
5
|
+
already reads from nativegate.yaml. A marked file without the flag must
|
|
6
|
+
fail with a message that names the flag, never a raw parser traceback.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import textwrap
|
|
10
|
+
|
|
11
|
+
from click.testing import CliRunner
|
|
12
|
+
|
|
13
|
+
from nativegate import cli
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DUAL = textwrap.dedent("""\
|
|
17
|
+
CS REAL FUNCTION GAMMA(X)
|
|
18
|
+
CD DOUBLE PRECISION FUNCTION DGAMMA(X)
|
|
19
|
+
CS REAL
|
|
20
|
+
CD DOUBLE PRECISION
|
|
21
|
+
1 C,CONV,EPS
|
|
22
|
+
DGAMMA = 24.0D0
|
|
23
|
+
RETURN
|
|
24
|
+
END
|
|
25
|
+
""")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_inspect_with_dialect_resolves_and_parses(tmp_path):
|
|
29
|
+
src = tmp_path / "gamma.f"
|
|
30
|
+
src.write_text(DUAL)
|
|
31
|
+
result = CliRunner().invoke(
|
|
32
|
+
cli.main, ["inspect", str(src), "--function", "DGAMMA", "--dialect", "cd"]
|
|
33
|
+
)
|
|
34
|
+
assert result.exit_code == 0, result.output
|
|
35
|
+
assert "function DGAMMA(X: float) -> float" in result.output
|
|
36
|
+
# the rejected CS half stays out of the IR: no standalone GAMMA function
|
|
37
|
+
assert "function GAMMA(" not in result.output
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_inspect_without_dialect_names_the_flag(tmp_path):
|
|
41
|
+
src = tmp_path / "gamma.f"
|
|
42
|
+
src.write_text(DUAL)
|
|
43
|
+
result = CliRunner().invoke(
|
|
44
|
+
cli.main, ["inspect", str(src), "--function", "DGAMMA"]
|
|
45
|
+
)
|
|
46
|
+
assert result.exit_code != 0
|
|
47
|
+
assert "--dialect cs" in result.output and "--dialect cd" in result.output
|
|
48
|
+
assert "Traceback" not in 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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|