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