Summary
Make realm_object (src/lib/common/adam_realm_object.F90) an abstract type and convert the 13 error-stop orchestrator-contract stubs (the _forest-suffixed TBPs) into deferred bindings. This moves the realm contract from runtime enforcement to compile-time enforcement: a realm that fails to implement the contract will no longer compile instead of error_stop-ing mid-run.
Scope is contract-only. Data repartition and seam-contract neutralization are explicitly deferred to follow-ups.
Motivation
forest_object drives realms polymorphically via class(realm_object) :: realm(:) and calls only the _forest contract methods on them. Today 13 of the 16 contract methods have default bodies that just:
call mpih%error_stop(msg='realm_object%<name>_forest: not overridden by app extension')
A realm (current or future) that forgets an override discovers it only when the simulation crashes mid-run. For the heterogeneous multi-realm forest now under construction (Forest campaign #10, #16, #17, #18), where different concrete realm types share one realm(:) array, that is precisely the place the failure must not be allowed to hide. Making the contract deferred turns 13 runtime failure modes into 13 compile-time errors.
This is also the lowest-friction on-ramp to a future traits-based realm: an all-deferred abstract type renames near-mechanically to a trait if/when the language gains one.
Decisions
- 13 deferred, 3 kept concrete. Only the 13 error-stop stubs become
deferred. The 3 methods carrying real default behaviour stay concrete, because prism_cpu_object relies on inheriting them:
finalize_mpi_forest → call mpih%finalize (CPU default; FNL overrides)
after_topology_build_forest → no-op (CPU default; FNL overrides)
coupling_descriptor_forest → sentinel nv = -1 (also overridden by prism_common_object)
- Keep ALL current data. All ~25 data components stay on the base for now. Data repartition is a separate follow-up.
Why this is non-breaking
prism_common_object is never instantiated directly (only extended) — safe to inherit abstract. Verified: no type(prism_common_object) / allocate(...common...).
- The only instantiated realm types are the leaves
prism_fnl_object and prism_cpu_object (drivers adam_prism_fnl.F90:33, adam_prism_cpu.F90:33, both type(...) not class(...)).
- Both leaves already override all 13 soon-to-be-deferred methods (FNL 14, CPU 13). Once deferred, the existing leaves already satisfy the contract — no new overrides required; expected to compile unchanged.
- Every contract call site is TBP-dispatched (
self%x_forest / realm(is)%x_forest). Verified: no bare module-procedure-name calls to any of the 13 — deleting the stub bodies breaks no caller.
- PRISM is the only consumer of
realm_object. NASTO/CHASE/PATCH do not extend it.
The 16 contract bindings (adam_realm_object.F90:150–166)
DEFER (13) — signatures copied verbatim from the live stub headers:
| Binding |
Header line |
Args after self |
initialize_forest |
378 |
filename character(*); realms_number, nv integer(I4P),optional; memory_avail real(R8P),optional; verbose logical,optional; self intent(inout) |
compute_local_dt_forest |
404 |
dt_local real(R8P),intent(out); self intent(in) |
advance_one_step_forest |
420 |
dt real(R8P),intent(in); self intent(inout) |
stages_per_step_forest |
447 |
function ... result(K), K integer(I4P); self intent(in) |
open_step_forest |
499 |
dt real(R8P),intent(in); self intent(inout) |
begin_stage_forest |
516 |
k, K_total integer(I4P),intent(in); dt real(R8P),intent(in); realm(:) class(realm_object),intent(inout),optional,target; self intent(inout) |
end_stage_forest |
544 |
as begin_stage_forest + flux_register class(flux_register_object),intent(inout),optional; self intent(inout) |
close_step_forest |
574 |
dt real(R8P),intent(in); self intent(inout) |
post_step_forest |
593 |
dt, t real(R8P),intent(in); it integer(I4P),intent(in); do_save_state, do_save_residuals, do_save_restart, do_amr logical,intent(in),optional; realm(:) class(realm_object),intent(inout),optional,target; self intent(inout) |
is_done_forest |
624 |
done logical,intent(out); self intent(in) |
finalize_forest |
642 |
(none); self intent(inout) |
fill_seam_from_peer_forest |
697 |
peer class(realm_object),intent(in),target; p_idx integer(I4P),intent(in); self intent(inout) |
apply_reflux_to_stage_forest |
720 |
stage integer(I4P),intent(in); dt real(R8P),intent(in); flux_register class(flux_register_object),intent(in); self intent(inout) |
KEEP CONCRETE (3) — bodies untouched: coupling_descriptor_forest (473–497), finalize_mpi_forest (659–673), after_topology_build_forest (708–718).
⚠️ The optional, target, and class(...) attributes on the sibling-realm(:) and flux_register dummies are load-bearing. The deferred interface must match each leaf override exactly. Copy each signature from the live stub header — do not retype from the table.
Implementation
1. src/lib/common/adam_realm_object.F90
a. Line 70 → type, abstract :: realm_object.
b. Lines 150–166: convert the 13 bindings from
procedure, pass(self) :: <name> to
procedure(<name>_interface), pass(self), deferred :: <name>. Leave the 3 concrete ones unchanged.
c. Add 13 named abstract interface blocks beside the existing 8 FDV interfaces (190–280), mirroring their idiom (import :: realm_object, I4P, R8P, + flux_register_object where needed), suffix _interface. Example (advance_one_step_forest, from 420–442):
abstract interface
subroutine advance_one_step_forest_interface(self, dt)
import :: realm_object, R8P
class(realm_object), intent(inout) :: self
real(R8P), intent(in) :: dt
end subroutine advance_one_step_forest_interface
end interface
d. Delete the 13 error-stop stub bodies (a deferred binding has no body): the module procedures at 378–402, 404–418, 420–445, 447–471, 499–514, 516–542, 544–572, 574–591, 593–622, 624–640, 642–657, 697–706, 720–752.
e. Leave untouched: the 3 concrete _forest bodies; ALL data components; the 8 FDV abstract interfaces (190–280) and their private FDV operator bodies; the IO TBPs; initialize / load_fdv_from_file.
2. src/app/prism/common/adam_prism_common_object.F90 — verify only
extends(realm_object) (line 29) now transitively inherits abstract; with unimplemented deferred bindings it is itself abstract — correct, and already the de-facto reality. No source change expected.
3. Leaf realms — expected no change
prism_fnl_object, prism_cpu_object already override all 13. A "deferred binding not overridden" error names a genuine gap the old runtime stub hid — fix by adding the override, never by re-adding a base default.
Out of scope (follow-ups)
Verification
Branch from develop (GitFlow). Build CPU first (it relies on the inherited defaults — primary risk):
fobis build --mode prism-cpu-gnu # expect clean
module load nvhpc
fobis build --mode prism-fnl-nvf --varset local_nvf # overrides the most, incl. the 3 concrete
fobis build --mode prism-cpu-gnu-debug # strict; catch any instantiate-of-abstract
Behaviour parity (compile-time-only change; no executable logic altered):
./exe/prism-cpu-gnu <existing prism input.ini> # N=1 fast path
src/tests/prism/regression/run-fnl-local.sh # multi-realm (WSL correctness-only, not perf)
Compare field L2 / L-inf norms to the pre-refactor reference — must match exactly.
Guard greps:
grep -n "not overridden by app extension" src/lib/common/adam_realm_object.F90 # only near the 3 kept methods
Success criteria
Related
Forest campaign: #10 (migration plan), #16 (α), #17 (γ), #18 (β). Realm-state hazards the static contract helps surface early: #11, #13.
Summary
Make
realm_object(src/lib/common/adam_realm_object.F90) anabstracttype and convert the 13 error-stop orchestrator-contract stubs (the_forest-suffixed TBPs) intodeferredbindings. This moves the realm contract from runtime enforcement to compile-time enforcement: a realm that fails to implement the contract will no longer compile instead oferror_stop-ing mid-run.Scope is contract-only. Data repartition and seam-contract neutralization are explicitly deferred to follow-ups.
Motivation
forest_objectdrives realms polymorphically viaclass(realm_object) :: realm(:)and calls only the_forestcontract methods on them. Today 13 of the 16 contract methods have default bodies that just:A realm (current or future) that forgets an override discovers it only when the simulation crashes mid-run. For the heterogeneous multi-realm forest now under construction (Forest campaign #10, #16, #17, #18), where different concrete realm types share one
realm(:)array, that is precisely the place the failure must not be allowed to hide. Making the contractdeferredturns 13 runtime failure modes into 13 compile-time errors.This is also the lowest-friction on-ramp to a future traits-based
realm: an all-deferredabstract type renames near-mechanically to a trait if/when the language gains one.Decisions
deferred. The 3 methods carrying real default behaviour stay concrete, becauseprism_cpu_objectrelies on inheriting them:finalize_mpi_forest→call mpih%finalize(CPU default; FNL overrides)after_topology_build_forest→ no-op (CPU default; FNL overrides)coupling_descriptor_forest→ sentinelnv = -1(also overridden byprism_common_object)Why this is non-breaking
prism_common_objectis never instantiated directly (only extended) — safe to inheritabstract. Verified: notype(prism_common_object)/allocate(...common...).prism_fnl_objectandprism_cpu_object(driversadam_prism_fnl.F90:33,adam_prism_cpu.F90:33, bothtype(...)notclass(...)).self%x_forest/realm(is)%x_forest). Verified: no bare module-procedure-name calls to any of the 13 — deleting the stub bodies breaks no caller.realm_object. NASTO/CHASE/PATCH do not extend it.The 16 contract bindings (
adam_realm_object.F90:150–166)DEFER (13) — signatures copied verbatim from the live stub headers:
selfinitialize_forestfilenamecharacter(*);realms_number, nvinteger(I4P),optional;memory_availreal(R8P),optional;verboselogical,optional; selfintent(inout)compute_local_dt_forestdt_localreal(R8P),intent(out); selfintent(in)advance_one_step_forestdtreal(R8P),intent(in); selfintent(inout)stages_per_step_forestfunction ... result(K),Kinteger(I4P); selfintent(in)open_step_forestdtreal(R8P),intent(in); selfintent(inout)begin_stage_forestk, K_totalinteger(I4P),intent(in);dtreal(R8P),intent(in);realm(:)class(realm_object),intent(inout),optional,target; selfintent(inout)end_stage_forestbegin_stage_forest+flux_registerclass(flux_register_object),intent(inout),optional; selfintent(inout)close_step_forestdtreal(R8P),intent(in); selfintent(inout)post_step_forestdt, treal(R8P),intent(in);itinteger(I4P),intent(in);do_save_state, do_save_residuals, do_save_restart, do_amrlogical,intent(in),optional;realm(:)class(realm_object),intent(inout),optional,target; selfintent(inout)is_done_forestdonelogical,intent(out); selfintent(in)finalize_forestintent(inout)fill_seam_from_peer_forestpeerclass(realm_object),intent(in),target;p_idxinteger(I4P),intent(in); selfintent(inout)apply_reflux_to_stage_foreststageinteger(I4P),intent(in);dtreal(R8P),intent(in);flux_registerclass(flux_register_object),intent(in); selfintent(inout)KEEP CONCRETE (3) — bodies untouched:
coupling_descriptor_forest(473–497),finalize_mpi_forest(659–673),after_topology_build_forest(708–718).Implementation
1.
src/lib/common/adam_realm_object.F90a. Line 70 →
type, abstract :: realm_object.b. Lines 150–166: convert the 13 bindings from
procedure, pass(self) :: <name>toprocedure(<name>_interface), pass(self), deferred :: <name>. Leave the 3 concrete ones unchanged.c. Add 13 named
abstract interfaceblocks beside the existing 8 FDV interfaces (190–280), mirroring their idiom (import :: realm_object, I4P, R8P,+ flux_register_objectwhere needed), suffix_interface. Example (advance_one_step_forest, from 420–442):d. Delete the 13 error-stop stub bodies (a
deferredbinding has no body): the module procedures at 378–402, 404–418, 420–445, 447–471, 499–514, 516–542, 544–572, 574–591, 593–622, 624–640, 642–657, 697–706, 720–752.e. Leave untouched: the 3 concrete
_forestbodies; ALL data components; the 8 FDV abstract interfaces (190–280) and their private FDV operator bodies; the IO TBPs;initialize/load_fdv_from_file.2.
src/app/prism/common/adam_prism_common_object.F90— verify onlyextends(realm_object)(line 29) now transitively inheritsabstract; with unimplemented deferred bindings it is itself abstract — correct, and already the de-facto reality. No source change expected.3. Leaf realms — expected no change
prism_fnl_object,prism_cpu_objectalready override all 13. A "deferred binding not overridden" error names a genuine gap the old runtime stub hid — fix by adding the override, never by re-adding a base default.Out of scope (follow-ups)
prism_common_object/leaves. High churn; deferred.select type(peer) class is (prism_*_object)downcasts (adam_prism_cpu_object.F90:1162,adam_prism_fnl_object.F90:1966) reaching into concrete peer device buffers. These block true heterogeneous coupling and need a neutral lib-typed exchange buffer (Forest campaign Forest α: end-of-step barrier seam coupling for asymmetric K across realms (AMReX-aligned) #16/Forest γ (research RFC): continuous (dense) output for full-order seam coupling under asymmetric K #17/Forest β: stage-coincident inter-realm seam coupling for same-K homogeneous realms #18).Verification
Branch from
develop(GitFlow). Build CPU first (it relies on the inherited defaults — primary risk):Behaviour parity (compile-time-only change; no executable logic altered):
Compare field L2 / L-inf norms to the pre-refactor reference — must match exactly.
Guard greps:
Success criteria
realm_objectisabstract; the 13 contract methods aredeferredwith matching named interfaces; the 3 real-default methods remain concrete.prism-cpu-gnu,prism-fnl-nvf, and-debugbuild cleanly with no new overrides added to the leaves._forestoverride) now fails to compile.Related
Forest campaign: #10 (migration plan), #16 (α), #17 (γ), #18 (β). Realm-state hazards the static contract helps surface early: #11, #13.