Skip to content

L-7b-ii follow-up: tot_init should cascade eq_api_init (avoid dual-flag foot-gun) #209

@k-yoshimi

Description

@k-yoshimi

Context

PR #207 (#201 Phase 2b) added a Layer-C smoke test test_bpsd_round_trip that drives:

tot_init()              # tot_api: g_initialized = True; eq's COMMON via tr_api_init -> CALL eq_init
eq_init()               # eq_api: g_initialized = True ← REQUIRED, separate flag
eq_set_param("MODELG", 3.0)
eq_set_param_str("KNAMEQ", "eqdata-HT6M")
eq_run(1)               # eq_load -> eq_bpsd_put
tr_check_bpsd_pull(ok)  # ok = 1

The eq_init() call is required because eq_api maintains its own g_initialized flag (eq/eq_api.f90:70) separate from tot_api's (tot/tot_api.f90:88). Without the explicit eq_init(), eq_set_param/eq_run return EQ_ERR_NOT_INIT (2).

tot_api_init calls equnit::eq_init (the Fortran subroutine for COMMON setup) via tr_api_init's CALL eq_init, but it does NOT call eq_api_init (the C ABI wrapper that flips eq_api's g_initialized).

Why this is a foot-gun

A user driving the mono image through tot's C ABI is reasonably expected to think tot_init brings up everything — but eq-side calls via the C ABI still need their own eq_init. Discoverability of this is poor:

  • Public docs say tot_init brings up all sub-modules.
  • tot_api_init does call equnit's eq_init internally (for COMMON state).
  • The C ABI export eq_api_init is a separate function, internally idempotent but its g_initialized flag is independent.

For Phase 2c, when users actually use mono in production via TotPipeline, this dual-init contract becomes a footgun:

  • A TotPipeline.run_pipeline([("eq",...),("tr",...)]) would call wrappers, each of which calls its own _api_init. The mono routing PR (#NNN) makes them all load the same .so. Should each wrapper still flip its private g_initialized? Probably yes — that's the wrapper's contract.
  • BUT a user writing direct ctypes code against the mono image (skipping TotPipeline) will hit this footgun.

Fix options

Option A: tot_api_init ALSO calls eq_api_init() after the existing tr_api_init(). Trivial 5-line change. Per-module g_initialized flags get flipped through the tot lifecycle. Subsequent direct calls to eq_set_param etc. don't need an extra eq_init() call.

Pros: cleanest user surface; mono direct-ctypes use case "just works"
Cons: deviates from current "tot_init initializes the COMMON state only, leaves C ABI flags untouched" pattern (if there is one)

Option B: Make eq_api's g_initialized track equnit_eq_init calls instead of being independent. I.e. any caller of equnit_eq_init flips eq_api's flag too. Requires a hook in equnit.f90.

Pros: less duplication of init code paths
Cons: invasive to existing Fortran (boundary concern per feedback_fortran_refactor_needs_lead_signoff.md)

Option C: Document the dual-init contract loudly in tot/tot_api.h and python/totlib/README.md. No code change.

Pros: zero risk
Cons: doesn't fix the footgun, just labels it

Recommendation

Option A is the right scope. Confirm with ats-fukuyama if uncertain, but adding a CALL eq_api_init() inside tot_api_init's init chain is additive and within the "k-yoshimi-led C ABI extension" boundary.

Effort

~30 min including local + CI verification.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions