nativegate
← Blog

First contact: wrapping netlib specfun end to end

Until last week every nativegate worked example lived in its own repo: the ~4,800-line petro library that ships with the project. A showcase like that is the easy case — whoever writes the binding generator and whoever owns the example library share a repo, a CI setup, and an author's assumptions, and neither party is ever really surprised. So the generator was pointed at someone else's code: netlib specfun, S. Zhang and J. Jin's 1996 Fortran collection of special functions (gamma, digamma, Dawson's integral, error functions, Bessel I0/I1/K0/K1), fetched straight from netlib.

The artifact this produced is specfun-py, and it is not a demo: the repository carries the upstream .f files, a COMMITTED golden tape pinning the return values of all 14 entry points, generated CMake and pytest, and CI on ubuntu and macOS. The claim of most interest to people who re-host numerical code is specific, so write it exactly below:

  • CI regenerates the bindings from the committed Fortran, from scratch, on every push — on ubuntu and macOS — and runs the generated pytest suite against the result.
  • The golden tape pins the return values of all 14 entry points (rtol 1e-9, atol 1e-12), plus the SHA-256 of every upstream source file. A rebuild that moves any number, or an edit that touches any byte, fails the build.

Actual numbers

erf, checked independently against the CPython standard library itself and matching bitwise over the pinned point, is the cleanest single indicator:

DERF(1.0)  = 0.8427007929497148   math.erf(1.0) = 0.8427007929497148
PSI(1.0)   = -0.5772156649015329  (negated Euler–Mascheroni, 16 digits)
DGAMMA(0.5)= 1.772453850905516    sqrt(pi)      = 1.7724538509055159
BESK0(1.0) = 0.4210244382407083   known value:  0.4210244382407083
DAW(1.0)   = 0.5380795069127684   known value:  0.5380795069127685
EI(1.0)    = 1.8951178163559368   known value:  1.8951178163559368

Thirty-year-old code agreeing with references to sixteen digits, with no Fortran written or edited here except one dialect choice, documented in PROVENANCE.md. erf is the cleanest single indicator: the check above is against CPython's own stdlib, not a re-paste of the same formula.

What broke — and why this matters

Three failures were produced by this contact, none of them invented; each is a contract hole the in-repo test suite never covered:

1. Service names that are not Python module names

ngate quickstart gamma.f --name specfun-gamma scaffolded, generated, and produced from ._native.specfun-gamma import … — invalid Python. The compile-step gate in the generator that checks generated code did refuse to write it, but three commands and a scaffold directory later. The scaffold commands now reject the name immediately, with the reason on the first line. This is a bug everyone hits on their first try, which is exactly the argument for third-party contact.

2. The CS/CD dialect trick

Netlib sources from this era tag machine precision by commenting every line in both dialects and asking the installer to comment out one:

CS    REAL FUNCTION GAMMA(X)
CD    DOUBLE PRECISION FUNCTION DGAMMA(X)

With both lines present, the fixed-form grammar sees a floating continuation with no statement. fparser2 refuses the whole file; the regex fallback accepts it and then finds no routine to expose, because the FUNCTION statements are marked too. Since nativegate 0.1.3 this is solved, not worked around: `dialect: cd` (or `cs`) in nativegate.yaml — or `--dialect cd` on quickstart — resolves the marking into the build copy, the chosen half live in place and the other half commented, with the untouched netlib bytes staying under native/ so the golden hashes still describe the upstream source. One hole in the first implementation (dialect-marked statement labels like CD900 DGAMMA = RES) was caught by actually building specfun-py from the pristine downloads and fixed in 0.1.5. The full loader is documented in nativegate's Fortran guide.

3. Generated packaging assumed generate had run

Every commit of a service tree that never ran generate breaks its own CMake reference to the INCLUDE-expansion directory and fails with "no known rule to make it". The consumer-side error was not even in the output nativegate owns. Workaround documented; a template-level fix is tracked (DEFECTS D11).

Why the golden tape is the load-bearing part

Four different stages stand between source and wheel: parse, expand, bind, build. Each one can be wrong in a way that ships a module that imports cleanly and answers with different numbers. Nothing that ships with a typical binding generator catches that, because the failure is invisible to import-time checks. The golden record is the counterweight: 14 entry points, tolerances, and a SHA-256 of every source byte. It costs one golden record command and one CI job, and it is what let us tell the three upstream failures apart from "probably fine" with no Fortran reading on our part.

The showcase repo → Next: generated vs hand-written bindings