Skip to content

refactor(realm): make realm_object an abstract behaviour-only contract (deferred _forest TBPs) #19

Description

@szaghi

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_forestcall 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

  • realm_object is abstract; the 13 contract methods are deferred with matching named interfaces; the 3 real-default methods remain concrete.
  • prism-cpu-gnu, prism-fnl-nvf, and -debug build cleanly with no new overrides added to the leaves.
  • An intentionally-incomplete test realm (omit one _forest override) now fails to compile.
  • Regression norms unchanged vs pre-refactor.

Related

Forest campaign: #10 (migration plan), #16 (α), #17 (γ), #18 (β). Realm-state hazards the static contract helps surface early: #11, #13.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions