Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,20 @@ jobs:
echo "::endgroup::"
done

- name: Run per-module dlopen C ABI drivers (X_api_check_so, #246)
# Each driver dlopens libXapi.so, checks X_state_abi_version() and
# X_state_size() against X_api.h (the Fortran BIND(C) type vs the
# C struct, #246) and runs the init/run/get_state/finalize cycle.
# The static-link drivers (X_api_check_all) stay local: tr's does
# not link on macOS and they need the non-PIC chain; the dlopen
# path is what the Python wrappers use.
run: |
for mod in tr fp ti wr wrx eq; do
echo "::group::${mod}_api_check_so"
make -C "$mod" "${mod}_api_check_so"
echo "::endgroup::"
done

- name: Run orchestrator Layer 2 + Layer 4 C ABI checks (tot_api_check_all)
# Phase L-6 composite target. Builds and runs every C ABI driver:
# tot_api_check -> test_smoke + test_param (Layer 3 dispatcher)
Expand Down Expand Up @@ -410,15 +424,18 @@ jobs:
run: |
make -C tot libtotapi_mono.so

- name: Inspect libtotapi_mono.so (8-bpsd + no-lib*api.so-deps hard-gated)
# libtotapi_mono_inspect runs TWO hard-gates plus several
- name: Inspect libtotapi_mono.so (8-bpsd + 12-layout-syms + no-lib*api.so-deps hard-gated)
# libtotapi_mono_inspect runs THREE hard-gates plus several
# observational checks:
#
# HARD GATE 1: BPSD storage symbol count == 8 (one per bpsd module
# type: device + equ1d + metric1d + plasmaf + shot + species +
# trmatrix + trsource). If the per-module dedup ever fails
# and we get 5×8=40 (or any other count), the build fails.
#
# HARD GATE 1b: the 12 X_state_abi_version / X_state_size symbols
# (eq tr fp ti wrx tot) are exported (#246).
#
# HARD GATE 2: No `lib*api.so` runtime dep in the .so's NEEDED
# list (Linux `ldd`) / load commands (macOS `otool -L`). The
# whole point of the mono build is that it has no per-module
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/).
unnoticed. The `make.header` heredoc and the `mtxp/make.mtxp` copy that
those two jobs carried as KEEP-IN-SYNC steps are now the composite action
`.github/actions/provision-build-env`, shared by all three.
- **C ABI: every library reports its state-struct layout, and the Python
wrappers refuse a mismatch (#246).** `X_state_abi_version()` returns
`X_STATE_ABI_VERSION` (now a `PARAMETER` in `X_state.f90`, mirrored by
the `#define` in `X_api.h`; tr and tot stay at 2, eq / fp / ti / wr / wrx
start at 1) and `X_state_size()` the `sizeof` of the `BIND(C)` type, both
callable before `X_init`, for tr / fp / eq / ti / wr / wrx / tot and the
mono image. The C-ABI drivers (`test_negative.c`, `test_run_so.c`) fail
when either differs from the header; `python/Xlib/_ffi.py::load_library`
raises `XlibAbiError` when the library's version or size differs from
`XStateC`, or when the library exports neither symbol (built before this
change), naming the fix. CI runs the six per-module dlopen drivers
(`X_api_check_so`) and `libtotapi_mono_inspect` checks the twelve symbols.
A library and a wrapper from different commits used to fail silently:
a longer library struct wrote past the wrapper's allocation, a shorter
one left the appended fields as plausible zeros. The check protects
callers from this change on; a caller older than it never asks, and
`X_get_state` still takes no buffer size.

### Changed

Expand Down
27 changes: 25 additions & 2 deletions docs/eq-library/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ parameters (`KNAMEQ`, `KNAMEQ2`, `KNAMWR`, `KNAMWM`, `KNAMFP`,
|---|---|---|
| 0. Regression baseline | `test_run/baselines/eq_*/metrics.json` | Phase L-0 ground truth (`eq_iter01`, `eq_tst2`) |
| 1. Fortran backend | `eq/eq_api.f90`, `eq_param_registry.f90`, `equnit`, `eqcalc`, ... | Numerics, shared with the `eq` binary |
| 2. C ABI | `eq/eq_api.h` + BIND(C) exports in `eq_api.f90` | 6 functions, stable contract |
| 2. C ABI | `eq/eq_api.h` + BIND(C) exports in `eq_api.f90` | 6 functions + 2 layout queries (#246), stable contract |
| 3. Shared library | `eq/libeqapi.so` | `make -C eq libeqapi.so`, links `lib*_pic.a` |
| 4. Python FFI | `python/eqlib/_ffi.py` | `ctypes` layout mirror of `eq_state_t` |
| 5. High-level wrapper | `python/eqlib/eqlib.py`, `state.py`, `errors.py` | Context manager + dataclass + exceptions |
| 6. User scripts | `python/eqlib/examples/*.py`, user notebooks | |

### 6 C ABI entry points
### 6 C ABI entry points (+ 2 layout queries, #246)

From `eq/eq_api.h`:

Expand Down Expand Up @@ -216,6 +216,29 @@ Mirrors the tr spec's §12.2 for the EQ module:
by the registry. Always use `set_param("PSIB[i]", v)` for
`i ∈ 0..5`.

## State struct layout rule (#246)

`eq_state_t` (`eq/eq_api.h`) and `eq_state_c` (`eq/eq_state.f90`) are
one fixed layout, filled by `eq_get_state`. Changing it:

1. append the field at the end of both (never reorder, never remove);
2. bump `EQ_STATE_ABI_VERSION` **in both files** (the `#define` and the
`PARAMETER`; currently `1`);
3. mirror the field in `python/eqlib/_ffi.py` (`EqStateC._fields_`) and
`state.py`;
4. rebuild `libeqapi.so` in the same commit.

The library exports its own view: `eq_state_abi_version()` returns the
PARAMETER and `eq_state_size()` the `sizeof` of the BIND(C) type, both
callable before `eq_init`. `eq/tests/c_abi/test_negative.c` and
`test_run_so.c` fail when either differs from the header, and
`python/eqlib/_ffi.py::load_library` refuses a library whose version or
size differs from `EqStateC`, or that exports neither symbol (built
before #246), with `EqlibAbiError` naming the fix. Both silent
failure modes of a stale pair are therefore caught at load time: a longer
library struct would have written past the wrapper's allocation, a
shorter one would have left the appended fields as plausible zeros.

## References

- Design spec: `docs/superpowers/specs/2026-04-17-tr-library-design.md`
Expand Down
27 changes: 25 additions & 2 deletions docs/fp-library/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ diagram and per-phase status.
|---|---|---|
| 0. Regression baseline | `test_run/baselines/fp_{iter01,jt60,dt1}/` | Phase 0 ground truth (tol `1e-10`) |
| 1. Fortran backend | `fp/fp_api.f90`, `fp_param_registry.f90`, `fpprep`, `fploop`, ... | Numerics, shared with `fp` binary |
| 2. C ABI | `fp/fp_api.h` + BIND(C) exports in `fp_api.f90` | 5 functions, stable contract |
| 2. C ABI | `fp/fp_api.h` + BIND(C) exports in `fp_api.f90` | 5 functions + 2 layout queries (#246), stable contract |
| 3. Shared library | `fp/libfpapi.so` | `make -C fp libs_pic && make -C fp libfpapi.so` |
| 4. Python FFI | `python/fplib/_ffi.py` | `ctypes` layout mirror of `fp_state_t` |
| 5. High-level wrapper | `python/fplib/fplib.py`, `state.py`, `errors.py` | Context manager + dataclass |
| 6. User scripts | `python/fplib/examples/*.py`, user notebooks | |

### 5 C ABI entry points
### 5 C ABI entry points (+ 2 layout queries, #246)

From `fp/fp_api.h`:

Expand Down Expand Up @@ -225,6 +225,29 @@ Mirrors the tr spec's §12.2 for the FP module:
- **Single instance per process.** FP globals are module-level
state; two concurrent `Fplib()` instances share FPCOMM.

## State struct layout rule (#246)

`fp_state_t` (`fp/fp_api.h`) and `fp_state_c` (`fp/fp_state.f90`) are
one fixed layout, filled by `fp_get_state`. Changing it:

1. append the field at the end of both (never reorder, never remove);
2. bump `FP_STATE_ABI_VERSION` **in both files** (the `#define` and the
`PARAMETER`; currently `1`);
3. mirror the field in `python/fplib/_ffi.py` (`FpStateC._fields_`) and
`state.py`;
4. rebuild `libfpapi.so` in the same commit.

The library exports its own view: `fp_state_abi_version()` returns the
PARAMETER and `fp_state_size()` the `sizeof` of the BIND(C) type, both
callable before `fp_init`. `fp/tests/c_abi/test_negative.c` and
`test_run_so.c` fail when either differs from the header, and
`python/fplib/_ffi.py::load_library` refuses a library whose version or
size differs from `FpStateC`, or that exports neither symbol (built
before #246), with `FplibAbiError` naming the fix. Both silent
failure modes of a stale pair are therefore caught at load time: a longer
library struct would have written past the wrapper's allocation, a
shorter one would have left the appended fields as plausible zeros.

## References

- Design spec: `docs/superpowers/specs/2026-04-17-tr-library-design.md`
Expand Down
17 changes: 13 additions & 4 deletions docs/sphinx/modules/tr/en/extending-tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,20 @@ consumers may need to be rebuilt or version-checked.
- **Per-radius / per-species profile loops** at
`:321-330` — for a new array field, follow the
`RN` / `RT` / `AJ` / `QP` loop pattern.
5. **Bump `TR_STATE_ABI_VERSION`** at `tr/tr_api.h:38`.
The current value is `2`; bump to the next integer
(currently `3`).
5. **Bump `TR_STATE_ABI_VERSION` in both places**: the
`#define` in `tr/tr_api.h` and the `PARAMETER` in
`tr/tr_state.f90` (#246). The current value is `2`;
bump to the next integer (currently `3`). The library
exports the PARAMETER through `tr_state_abi_version()`
and its `sizeof` through `tr_state_size()`;
`tr/tests/c_abi/test_negative.c` fails when the two
values drift from the header, and `python/trlib/_ffi.py`
refuses a `libtrapi.so` whose version or size differs
from `TrStateC` (`TrlibAbiError`), so a forgotten bump
or a stale build surfaces at load time instead of as
silent zeros.
6. **Mirror in the ctypes side** at
`python/trlib/_ffi.py:94-120` — append a tuple to
`python/trlib/_ffi.py` (`TrStateC._fields_`) — append a tuple to
`TrStateC._fields_`. Use the same field order as the
BIND-C struct so the two layouts stay byte-compatible.
7. **Surface in the Python `TrState` dataclass** at
Expand Down
15 changes: 11 additions & 4 deletions docs/sphinx/modules/tr/ja/extending-tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,18 @@ numbering 規約 (`tr/trcoef_turbulence.f90:392-398` の
- **per-radius / per-species profile loops** が
`:321-330` — 新配列 field は `RN` / `RT` / `AJ` /
`QP` の loop パターンに従う.
5. **`TR_STATE_ABI_VERSION` を bump する.**
`tr/tr_api.h:38` で. 現値は `2`; 次の整数 (現在の
流れだと `3`) に上げる.
5. **`TR_STATE_ABI_VERSION` を 2 箇所で bump する.**
`tr/tr_api.h` の `#define` と `tr/tr_state.f90` の
`PARAMETER` (#246). 現値は `2`; 次の整数 (現在の流れだと
`3`) に上げる. ライブラリは PARAMETER を
`tr_state_abi_version()` で, `sizeof` を `tr_state_size()`
で export し, `tr/tests/c_abi/test_negative.c` は header と
ずれると落ちる. `python/trlib/_ffi.py` は version か size が
`TrStateC` と違う `libtrapi.so` を load 時に拒否する
(`TrlibAbiError`) ので, bump 忘れや古いビルドは黙った 0 では
なく load 時のエラーとして現れる.
6. **ctypes 側にミラー.**
`python/trlib/_ffi.py:94-120` の `TrStateC._fields_` に
`python/trlib/_ffi.py` (`TrStateC._fields_`) の `TrStateC._fields_` に
tuple を append. BIND-C struct と同じ field 順を使う
ことで両 layout が byte-compatible に保たれる.
7. **Python `TrState` dataclass に表面化.**
Expand Down
Loading
Loading