diff --git a/capgen/generator/group_cap.py b/capgen/generator/group_cap.py index a6e0069d..859104d9 100644 --- a/capgen/generator/group_cap.py +++ b/capgen/generator/group_cap.py @@ -1136,7 +1136,7 @@ def _generate_state_alloc(suite_name: str, group_name: str) -> List[str]: so when multiple instances initialize, only the first allocates and initialises the state array; subsequent calls return immediately to avoid clobbering peer-instance state slots. Matches the - ``_suite_state_alloc`` pattern. + ``suite_state_alloc`` pattern. """ # Short Fortran symbol; the module ``ccpp___cap`` # already namespaces this routine at link time. diff --git a/capgen/generator/host_cap.py b/capgen/generator/host_cap.py index 683d5f4a..ba3e5ccf 100644 --- a/capgen/generator/host_cap.py +++ b/capgen/generator/host_cap.py @@ -45,6 +45,7 @@ from typing import Dict, List, Optional, Set, Tuple from metadata.parse_tools import CCPPError, open_if_changed +from metadata.metadata_table import FORTRAN_MAX_IDENT_LEN from metadata.variable_resolver import SchemeStore from generator.suite_resolver import ( ResolvedArg, @@ -1026,14 +1027,29 @@ def _generate_host_cap( use_lines: List[str] = [] for sname in suite_names: suite_cap_mod = 'ccpp_{}_cap'.format(sname) + # Suite cap symbols carry no suite prefix (issue #786), so every + # suite cap exports the same names. Rename on import to keep the + # N suites in this scope distinct. suite_subs = [] - suite_subs.append('{}_register'.format(sname)) - suite_subs.append('{}_init'.format(sname)) - for phase in _PHYSICS_PHASES: - suite_subs.append('{}_physics_{}'.format(sname, phase)) - suite_subs.append('{}_final'.format(sname)) - syms = ', '.join(suite_subs) - use_lines.append('{}use {}, only: {}'.format(_INDENT, suite_cap_mod, syms)) + aliased = [('{}_register'.format(sname), 'suite_register'), + ('{}_init'.format(sname), 'suite_init')] + aliased += [('{}_physics_{}'.format(sname, p), 'suite_physics_{}'.format(p)) + for p in _PHYSICS_PHASES] + aliased.append(('{}_final'.format(sname), 'suite_final')) + for alias, target in aliased: + if len(alias) > FORTRAN_MAX_IDENT_LEN: + raise CCPPError( + "Suite name '{}' is too long: the host cap imports the " + "suite cap's '{}' as '{}' ({} characters), over Fortran's " + "{}-character limit for names".format( + sname, target, alias, len(alias), FORTRAN_MAX_IDENT_LEN + ) + ) + suite_subs.append('{} => {}'.format(alias, target)) + use_lines.append('{}use {}, only: &'.format(_INDENT, suite_cap_mod)) + for i, sym in enumerate(suite_subs): + sep = ', &' if i < len(suite_subs) - 1 else '' + use_lines.append('{}{}{}'.format(_INDENT * 2, sym, sep)) ensure_error_unit_use(use_lines, _INDENT) lines.extend(use_lines) diff --git a/capgen/generator/suite_cap.py b/capgen/generator/suite_cap.py index 3ac958d6..75a23c28 100644 --- a/capgen/generator/suite_cap.py +++ b/capgen/generator/suite_cap.py @@ -7,13 +7,15 @@ * Imports all group cap modules and the constituent property module. * Exposes eight public entry points: - - ``_register`` — calls each scheme's ``_register`` to populate - the host-owned ``ccpp_model_constituents_t`` object. - - ``_init`` / ``_final`` — framework setup / teardown. - - ``_physics_init``, ``_physics_timestep_init``, - ``_physics_run``, ``_physics_timestep_final``, - ``_physics_final`` — dispatch by ``group_name`` to the - appropriate group cap subroutine. + - ``suite_register`` — calls each scheme's ``_register``. + - ``suite_init`` / ``suite_final`` — framework setup / teardown. + - ``suite_physics_init``, ``suite_physics_timestep_init``, + ``suite_physics_run``, ``suite_physics_timestep_final``, + ``suite_physics_final`` — dispatch by ``group_name`` to the + appropriate group cap subroutine. These carry no suite prefix: the + module name already supplies it, and repeating it overran Intel's + mangled-global-name limit for long suite names (issue #786). The + host cap renames them back to ``_`` on import. The static API (``_ccpp_cap.F90``) dispatches by ``suite_name`` to these subroutines. @@ -23,7 +25,7 @@ import os from typing import Dict, List, Optional, Set -from metadata.parse_tools import open_if_changed +from metadata.parse_tools import CCPPError, open_if_changed from metadata.variable_resolver import HostVarEntry, SchemeStore from generator.suite_resolver import ( ResolvedArg, @@ -57,6 +59,12 @@ # Canonical set of physics phases, always dispatched by the suite cap. _PHYSICS_PHASES = ('init', 'timestep_init', 'run', 'timestep_final', 'final') +_SUITE_CAP_OWN_SYMS = frozenset( + ['suite_physics_{}'.format(_p) for _p in _PHYSICS_PHASES] + + ['suite_register', 'suite_init', 'suite_final', + 'suite_state_alloc', 'suite_state_dealloc'] +) + # Constituent type / module constants. _CONST_MOD = 'ccpp_constituent_prop_mod' _CONST_DDT = 'ccpp_model_constituents_t' @@ -405,7 +413,8 @@ def _register_lines( Minimal signature: ``(instance_number, number_of_instances, errmsg, errflg)`` (the instance pair is included only when the host declares it). """ - sub_name = '{}_register'.format(suite_name) + sub_name = 'suite_register' + sub_label = '{}_register'.format(suite_name) i1 = _INDENT i2 = _INDENT * 2 @@ -482,7 +491,7 @@ def _register_lines( # compilers don't flag intent(in) args as unused when the gate is off. extra_in = [ninstances_local] if ninstances_local else None trace_lines = emit_trace_block( - sub_name, [], i2, + sub_label, [], i2, instance_local=inst_local, extra_in_names=extra_in, ) if trace_lines: @@ -497,7 +506,7 @@ def _register_lines( ] # Allocate state and DDT array on first call (idempotent). - suite_alloc_sub = '{}_suite_state_alloc'.format(suite_name) + suite_alloc_sub = 'suite_state_alloc' lines.append('{}call {}({}, {}, {})'.format( i2, suite_alloc_sub, ninstances_arg, errmsg_local, errflg_local )) @@ -652,7 +661,8 @@ def _init_lines( Minimal signature: ``(instance_number, number_of_instances, errmsg, errflg)`` -- the instance pair is included only when the host declares it. """ - sub_name = '{}_init'.format(suite_name) + sub_name = 'suite_init' + sub_label = '{}_init'.format(suite_name) i1 = _INDENT i2 = _INDENT * 2 @@ -704,7 +714,7 @@ def _init_lines( ] extra_in = [ninstances_local] if ninstances_local else None trace_lines = emit_trace_block( - sub_name, [], i2, + sub_label, [], i2, instance_local=inst_local, extra_in_names=extra_in, ) if trace_lines: @@ -718,7 +728,6 @@ def _init_lines( ] # State guard: must be in REGISTERED state (or already INITIALIZED — idempotent). - sub_label = '{}_init'.format(suite_name) lines += [ '{}if (.not. allocated(ccpp_suite_state)) then'.format(i2), "{} {} = '{}: ccpp_register has not been called'".format( @@ -811,7 +820,8 @@ def _final_lines( symmetry with ``_register`` / ``_init``; the framework does not consume it at final time. """ - sub_name = '{}_final'.format(suite_name) + sub_name = 'suite_final' + sub_label = '{}_final'.format(suite_name) i1 = _INDENT i2 = _INDENT * 2 @@ -865,7 +875,7 @@ def _final_lines( ] extra_in = [ninstances_local] if ninstances_local else None trace_lines = emit_trace_block( - sub_name, [], i2, + sub_label, [], i2, instance_local=inst_local, extra_in_names=extra_in, ) if trace_lines: @@ -926,7 +936,7 @@ def _final_lines( i2, dealloc_sub, errmsg_local, errflg_local )) lines.append('{} if ({} /= 0) return'.format(i2, errflg_local)) - suite_dealloc_sub = '{}_suite_state_dealloc'.format(suite_name) + suite_dealloc_sub = 'suite_state_dealloc' lines.append('{} call {}({}, {})'.format( i2, suite_dealloc_sub, errmsg_local, errflg_local )) @@ -958,7 +968,7 @@ def _physics_dispatch_lines( suite_res: SuiteResolution, host_dict=None, ) -> List[str]: - """Generate a ``_physics_`` dispatch subroutine. + """Generate a ``suite_physics_`` dispatch subroutine. The subroutine signature is derived entirely from the host's ``type=control`` metadata (all control variables except ``suite_name``, which is consumed at the @@ -966,7 +976,8 @@ def _physics_dispatch_lines( body uses a ``select case`` dispatch; otherwise all groups are called unconditionally. """ - sub_name = '{}_physics_{}'.format(suite_name, phase) + sub_name = 'suite_physics_{}'.format(phase) + sub_label = '{}_physics_{}'.format(suite_name, phase) i1 = _INDENT i2 = _INDENT * 2 i3 = _INDENT * 3 @@ -1014,7 +1025,7 @@ def _physics_dispatch_lines( # Trace block: references every intent(in)/inout control dummy so that # strict compilers don't flag any of them as unused. - trace_lines = emit_trace_block(sub_name, ctrl_entries, i2) + trace_lines = emit_trace_block(sub_label, ctrl_entries, i2) if trace_lines: lines.append('') lines.extend(trace_lines) @@ -1031,7 +1042,6 @@ def _physics_dispatch_lines( lines.append('') inst_idx = _instance_idx(host_dict) - sub_label = '{}_physics_{}'.format(suite_name, phase) if phase == 'final': # ``physics_final`` is silently idempotent: a repeat call (or a # call issued after ``ccpp_final``) must return cleanly with @@ -1107,7 +1117,6 @@ def _emit_group_call(resolved_group, indent): # control table there's nowhere to write the message, so skip # emission rather than silently swallow. if errflg_local and errmsg_local: - sub_label = '{}_physics_{}'.format(suite_name, phase) lines.append('{}case default'.format(i2)) lines.append('{}{} = 1'.format(i3, errflg_local)) lines.append( @@ -1131,7 +1140,7 @@ def _suite_state_alloc_lines( suite_name: str, has_suite_vars: bool, ) -> List[str]: - """Generate the ``_suite_state_alloc`` subroutine. + """Generate the ``suite_state_alloc`` subroutine. Idempotent allocator for the per-instance suite state array and the suite-owned DDT array. Inner allocatable fields inside the DDT are NOT @@ -1139,7 +1148,7 @@ def _suite_state_alloc_lines( called from ``_init`` after register-phase scheme calls have set any suite-owned scalar dimensions. """ - sub_name = '{}_suite_state_alloc'.format(suite_name) + sub_name = 'suite_state_alloc' data_alloc = 'suite_data_alloc' data_mod = 'ccpp_{}_data'.format(suite_name) i1 = _INDENT @@ -1178,8 +1187,8 @@ def _suite_state_dealloc_lines( suite_name: str, has_suite_vars: bool, ) -> List[str]: - """Generate the ``_suite_state_dealloc`` subroutine.""" - sub_name = '{}_suite_state_dealloc'.format(suite_name) + """Generate the ``suite_state_dealloc`` subroutine.""" + sub_name = 'suite_state_dealloc' data_dealloc = 'suite_data_dealloc' data_mod = 'ccpp_{}_data'.format(suite_name) i1 = _INDENT @@ -1264,6 +1273,19 @@ def _generate_suite_cap( ] syms_list.append('{}_state_alloc'.format(resolved_group.group_name)) syms_list.append('{}_state_dealloc'.format(resolved_group.group_name)) + # This module's own symbols are suite-name-free (issue #786), so a + # group named e.g. ``suite`` or ``suite_physics`` would export the + # same names it defines. + clash = sorted(set(syms_list) & _SUITE_CAP_OWN_SYMS) + if clash: + raise CCPPError( + "Suite '{}' has a group named '{}' whose cap exports {}, " + "colliding with the suite cap's own subroutine(s); rename " + "the group".format( + suite_name, resolved_group.group_name, + ', '.join(repr(c) for c in clash), + ) + ) use_lines.append('{}use {}, only: {}'.format( _INDENT, group_cap_mod, ', '.join(syms_list) )) @@ -1281,13 +1303,13 @@ def _generate_suite_cap( # points are always emitted. ``ccpp_register`` is mandatory in the new # design — even an empty register phase fires the state transition. pub_subs = [] - pub_subs.append('{}_register'.format(suite_name)) - pub_subs.append('{}_init'.format(suite_name)) + pub_subs.append('suite_register') + pub_subs.append('suite_init') for phase in _PHYSICS_PHASES: - pub_subs.append('{}_physics_{}'.format(suite_name, phase)) - pub_subs.append('{}_final'.format(suite_name)) - pub_subs.append('{}_suite_state_alloc'.format(suite_name)) - pub_subs.append('{}_suite_state_dealloc'.format(suite_name)) + pub_subs.append('suite_physics_{}'.format(phase)) + pub_subs.append('suite_final') + pub_subs.append('suite_state_alloc') + pub_subs.append('suite_state_dealloc') for sub in pub_subs: lines.append('{}public :: {}'.format(_INDENT, sub)) diff --git a/doc/briefing.md b/doc/briefing.md index 20b05a39..4fb6283b 100644 --- a/doc/briefing.md +++ b/doc/briefing.md @@ -289,6 +289,14 @@ The headline items for a reader of this brief: gating the framework setter additions. `followups.md` FU-020, FU-003. - **Codegen-time scheme-registration cross-check** — today's check is at runtime. FU-002. +- **Group-cap symbol-length budget** — the suite cap is guarded and + tested at its ceiling; the group cap's joint `suite`/`group` budget + is not. FU-037. +- **What the constituent metadata flags may legally be attached to** — + only a deliberately thin guard is enforced today (`index_of_*` may + not carry one). A stricter shape rule was written and rejected: it + would have encoded today's constituent storage in the metadata + parser. FU-036. - **Nested-subcycle `ccpp_loop_counter` semantics** — resolves to the outermost counter. FU-001. - **Transient shims** — `--legacy-mode`, `--gfs-dim-aliases`, @@ -417,8 +425,8 @@ don't rebuild downstream objects unless something actually moved. - The three incoming hosts **still rely on the transient migration shims** (§6.3b). Adoption did *not* retire them — it pinned them. See the note in that section and FU-010/FU-011. -- **Unit tests**: 1564 passing on `feature/capgen-v1` (verified - 2026-09-01, `python unit-tests/run_tests.py`). +- **Unit tests**: 1577 passing on `feature/capgen-v1` (verified + 2026-09-16, `python unit-tests/run_tests.py`). - **End-to-end tests passing** (13): `advection`, `advection_auto_clone`, `capgen`, `capgen_ng`, `chunked_data`, `constituents_dim`, `ddthost`, `instances`, `instances_advection`, @@ -484,7 +492,13 @@ don't rebuild downstream objects unless something actually moved. served (§1). The anticipated complication was "fast physics" called directly from the FV3 dynamical core as a separate group; **that group works with v1 as expected**, so no special handling - was needed. + was needed. First production defect report arrived 2026-09-16 + (issue #786): Intel rejected the generated suite-cap symbols as + `Global name too long` under `WARN_AS_ERROR`. The code was + standard-conforming, but capgen v1 had spent 11 characters of + linker-symbol budget that `ccpp-prebuild` did not, so a suite name + that used to build no longer did. Fixed the same day by removing + the suite name from the suite-cap subroutines; `followups.md` §5. - **CAM-SIMA**: **still on capgen v0 — the remaining transition, and the critical path.** As of 2026-09-01 CAM-SIMA production builds with the original ccpp-capgen; capgen v1 support lives on diff --git a/doc/briefing_pm.md b/doc/briefing_pm.md index 5d508fdd..fb264c0b 100644 --- a/doc/briefing_pm.md +++ b/doc/briefing_pm.md @@ -291,7 +291,7 @@ Features that exist only in capgen (some exist in prebuild): and the retirement of the CAM-SIMA compatibility layer) to programme-level blockers rather than CAM-SIMA-local work. Schedule risk concentrates there; see §8. -- **Unit tests**: 1564 passing. No known failures. +- **Unit tests**: 1577 passing. No known failures. - **End-to-end tests**: 13 passing — `advection`, `advection_auto_clone` (CAM-SIMA advection_test port exercising the auto-clone shim), `capgen`, `capgen_ng`, `chunked_data`, @@ -392,9 +392,10 @@ proposals are implementable on top of it. | capgen diverges from capgen feature set | LOW | Cross-checked by `doc/redesign_analysis.md`; the feature comparison table in §4 / §5 is exhaustive | | Host metadata break for UFS / NEPTUNE / CAM-SIMA | LOW | Three transient shims (`--legacy-mode`, `--gfs-dim-aliases`, `--legacy-auto-clone-constituents`) together cover the known-incompatible standard-name pair, the GFS radiation/composition vertical-dim spellings, and original capgen's auto-clone registration path. Remaining required changes (e.g., `_finalize` → `_final`) are mechanical and listed in `doc/migration.md` §3 | | Constituent overhaul stalls | LOW | Proposal A unblocks the immediate bug; capgen works with the current framework today; `--legacy-auto-clone-constituents` lets CAM-SIMA's atmospheric_physics build without an overhaul decision; the overhaul is a separate decision track | -| Bus-factor on capgen itself | MEDIUM | Procedural code style + flat data classes + 1564-test safety net; significantly lower than capgen's bus factor | +| Bus-factor on capgen itself | MEDIUM | Procedural code style + flat data classes + 1577-test safety net; significantly lower than capgen's bus factor | | **CAM-SIMA transition slips, delaying the whole programme** | **MEDIUM — the main schedule risk as of 2026-09-01** | The `develop` merge and the deletion of both older generators are gated on this one transition (§6), so its two remaining items — the constituent-ordering re-baseline and retirement of the compatibility layer — are programme-level blockers. Both are understood and scoped; neither is a defect. Mitigation is to track them as such rather than as CAM-SIMA-local work, and to decide the auto-clone shim's fate as part of the transition | | Two host call-shape conventions (prebuild-style vs capgen-style) coexist forever | LOW | capgen emits one shape; downstream host conversions are tracked in `doc/migration.md` | +| First production defect from a transitioned host | CLOSED (2026-09-16) | UFS reported issue #786 — Intel rejected generated symbol names as too long when the UFS build turned on warnings-as-errors. Diagnosed, fixed and tested the same day. Notable for two reasons: the generated code was standard-conforming (an Intel linker limit, not a Fortran violation), **and** it was a regression against `ccpp-prebuild`, which had used shorter names — so the transition, not the compiler, was the trigger. The class is now guarded by a test that generates at the supported limit rather than at any one model's suite name | | Regression discovered during NEPTUNE / UFS testing | LARGELY RETIRED (2026-09-01) | Both models have transitioned; NEPTUNE passes ~300 regression tests on three compilers including high-altitude physics, and the anticipated UFS FV3 fast-physics complication did not materialise. The SCM proving-ground approach worked as intended — issues became capgen fixes, not host-side patches | | ccpp-prebuild end-of-life requires a sunset plan | SCOPED (2026-09-01) | Decided: `ccpp-prebuild` **and** the older `ccpp-capgen` are both deleted from the framework repo in the same operation that merges capgen into `develop`, triggered when/after CAM-SIMA transitions. prebuild already has no production consumer. The residual risk is schedule, not scope — see the CAM-SIMA row above | diff --git a/doc/followups.md b/doc/followups.md index 4c8e03a4..668c54ed 100644 --- a/doc/followups.md +++ b/doc/followups.md @@ -51,6 +51,8 @@ Status values: `open`, `in progress`, `blocked`, `closed`. | FU-033 | Vertical flip on an allocatable host array is rejected, not supported | framework | 2026-08-12 | open | `_resolve_one_arg` (`capgen/generator/suite_resolver.py`, just after `needs_vert_flip` is computed) hard-errors when a host variable is `allocatable = True` **and** needs a vertical flip (host/scheme `top_at_one` disagree on a var with a vertical dim). An allocatable actual must omit subscripts, so the reverse-stride flip subscript cannot be encoded; silently dropping it would hand the scheme vertically-reversed data. **Decision (2026-08-12, PR #762 review finding from jimmielin): error for now** rather than emit a wrong-but-compiling cap. Test: `unit-tests/test_suite_resolver.py::TestVerticalFlipTransform::test_allocatable_host_plus_flip_raises`. Any future support is limited to the allocatable-host → *non-allocatable* (plain assumed-shape) scheme-dummy sub-case, where a flipped section `host(:, ub:lb:-1)` is legal; an allocatable dummy can never receive a flipped section. Parallels FU-032 (guard now, enhance later). | | FU-034 | Merge `feature/capgen-v1` to `develop` and delete capgen v0 + `ccpp-prebuild` — gated on CAM-SIMA | framework | 2026-09-01 | open | **Decision (Dom, 2026-09-01): the merge to `develop`, and the removal of both capgen v0 and `ccpp-prebuild` from the tree, happen WHEN/AFTER CAM-SIMA transitions to capgen v1.** One operation, one trigger — the sequencing is settled, not open. Context: NEPTUNE, CCPP-SCM and UFS transitioned on 2026-09-01 and build directly off **`feature/capgen-v1`** (`briefing.md` §10); CAM-SIMA is still on capgen v0 with v1 on testing/review branches, and is the last host holding either predecessor alive. Consequences: (1) **CAM-SIMA's transition is the critical path for the entire v1 rollout**, which promotes its gating items — the constituent-ordering re-baseline (FU-018/FU-030) and the compat-layer removal plan (FU-013) — from CAM-SIMA-local concerns to blockers on the whole merge; (2) until then `feature/capgen-v1` is a long-lived release branch with three production consumers, so treat it as released — no force-push, no breaking generated-API change without notice; (3) PR #762 is the umbrella PR and the natural vehicle; (4) `--legacy-auto-clone-constituents` (FU-012) is CAM-SIMA's shim, so it also sits on this critical path — decide it as part of the transition rather than inheriting it (§3). | | FU-035 | Constituent index evidence is scheme-metadata-only — a runtime-registered constituent nothing flags needs a host declaration | framework | 2026-09-01 | open | Fallout of the `index_of_*` fix (2026-09-01): `index_of_` is auto-provisioned as a constituent index only when capgen has **positive evidence** that X is a constituent, i.e. some scheme flags `X` or `tendency_of_X` `advected`/`constituent`/`molar_mass` (`_is_known_constituent`, `capgen/generator/suite_resolver.py`). That is the only constituent knowledge available at codegen — register-phase Fortran `%instantiate(std_name=…)` is not parsed (FU-002). **Residual gap:** a constituent registered at runtime whose base name is flagged in *no* scheme metadata, and whose index some scheme consumes, now raises the missing-provider error instead of resolving. Workaround (and what every in-tree host already does): declare the index in host metadata — host declarations win before any constituent path. No such case exists in CAM-SIMA, CCPP-SCM or the e2e corpus today; all nine CAM-SIMA `index_of_*` names are either registry-declared or scheme-produced `intent=out`. Closing FU-002 would remove the gap entirely by making registration itself the evidence. | +| FU-036 | Metadata-level validation of the constituent flags is deliberately minimal — decide the real rule at the overhaul | framework | 2026-09-03 | open | Raised by @peverwhee in PR #780 review: if a scheme sets `constituent = True` on something that cannot be a constituent (her example: `index_of_shortwave_band`), capgen ignored the flag and treated the variable as ordinary. **What landed** is a deliberately thin guard — `MetaVar.validate` rejects a constituent flag (`advected`/`constituent`/`molar_mass`) on a standard name starting with `index_of_`, on the ground that `index_of_` names an *index*, not a field (`capgen/metadata/metadata_table.py`, just after the FU-014 `protected` check; test `test_index_of_cannot_be_a_constituent`). It makes no claim about what a constituent *is*, so it survives whatever FU-020 decides; if `index_of_` is ever retired as a convention it becomes dead code and comes out in one line. **What was tried and REJECTED (Dom, 2026-09-03):** a rule requiring every constituent-flagged variable to be `real` with rank 2. It is *true* of all 207 constituent-flagged entries in this repo and both cam-sima trees, and it is forced by today's storage — a constituent is the slice `vars_layer(, , index_of_X)` of `real(kind_phys), allocatable :: vars_layer(:,:,:)` (`capgen/src/ccpp_constituent_prop_mod.F90:167`). **Rejected precisely because it is true only of today's storage**: baking the current constituent representation into the metadata parser pre-empts the overhaul, and a parse-time rule is the hardest kind to walk back, since it rejects input rather than routing it differently. A weaker "must not be a scalar" variant was also rejected — it catches index variables only by coincidence, and the coincidence does not hold: `index_of_steering_level_due_to_moving_mountain_gravity_wave_drag` is `real` **rank 1** in both cam-sima trees (`schemes/gravity_wave_drag/gravity_wave_drag_moving_mountain.meta`), so it would slip through. **Tripwire:** `unit-tests/test_metadata_table.py::TestConstituentAttributes._scheme_var` was deliberately left declaring `dimensions = ()` — a scalar "constituent". Any future shape rule will fail those three tests, which is the intended signal, not a defect. **Still open:** the general question — should a constituent flag on *any* variable that cannot be one be an error? Answering it means deciding what a constituent is, i.e. FU-020 / §2. Sibling of FU-035 (same fix, same PR). | +| FU-037 | No generation-time guard on the group cap's mangled global-name budget | framework | 2026-09-16 | open | The #786 fix gave the *suite* cap a guard (`host_cap.py`, `FORTRAN_MAX_IDENT_LEN` on the use-rename alias) and a test at the ceiling (`unit-tests/test_suite_cap.py::TestMangledGlobalNameLength`). The **group** cap has neither. Its global is `ccpp___cap_mp__TIMESTEP_FINAL`, a *joint* constraint `len(suite) + 2*len(group) <= 61` — the group name is counted twice, once in the module name and once in the subroutine name, and unlike the suite cap that repetition cannot be removed (the group name is what distinguishes the imported symbols inside the suite cap). Comfortable today (longest UFS pair is 29 + 2*12 = 53 for `FV3_GFS_v17_coupled_p8_ugwpv1` / `fast_physics`), but a suite name at capgen's 40-char ceiling needs group names of 10 or fewer, and nothing says so at generation time — the failure surfaces as an Intel diagnostic at compile, or silently at link on a compiler that truncates without complaint. Add the same guard + test for the group cap. See §5. | --- @@ -142,6 +144,28 @@ fire in a production build — but the Derecho aux tests are what prove it Namelist variables are protected too (`create_readnl_files.py:422, :440`), read-only by construction. +**Generated symbol names have a budget no standard describes.** Intel +truncates the mangled global name ``_mp_`` at 90 +characters. Fortran 2018 caps *names* at 63 and says nothing about the +linker symbol, so conforming code can still be rejected — this is what +NCAR/ccpp-framework#786 was. Consequences, all from the 2026-09-16 fix: + +* Suite-cap subroutines carry **no suite prefix** (`suite_register`, + `suite_init`, `suite_final`, `suite_physics_`, + `suite_state_(de)alloc`); the module name already supplies it. The host + cap use-renames them back to `_` on import, so its call sites + and the host-facing API (`ccpp_physics_` on `_ccpp_cap`) are + unchanged. Group caps and `ccpp__data` were already prefix-free. +* **Longest supported suite name is 40 characters**, bound by that + use-rename alias against Fortran's 63-char name limit — not by the + mangled global, which would allow 49. Enforced at generation time. +* The group cap adds a joint constraint, `len(suite) + 2*len(group) <= 61`, + which is **not** enforced — FU-037. +* Trace strings and runtime error messages keep the fully-qualified + `_` form via a separate `sub_label`; shortening a symbol must + never shorten the human-readable label. A backtrace is unaffected either + way: it prints the mangled symbol, which embeds the module name. + ## 6. Regenerating CAM-SIMA golden test files `test/unit/python/test_write_init_files.py` compares generated output @@ -167,6 +191,8 @@ file, per the procedure in the repository's `CLAUDE.md`. | Machine | Last reconciled | By | |---------|-----------------|-----| +| `dutchman` | 2026-09-16 | Issue #786 (UFS, @gspetro-NOAA): Intel `#5462 Global name too long` on `ccpp__cap_mp__PHYSICS_TIMESTEP_INIT`. Diagnosis: conforming code — Fortran caps *names* at 63, the mangled linker symbol is unspecified — **but a regression capgen v1 introduced**, since ccpp-prebuild abbreviated the phase (`tsinit`/`tsfinal`) and produced 82/83 characters where v1 produced 93/94, dropping the effective suite-name ceiling from 32 to 27 against a 29-character suite. Fixed by removing the suite name from every suite-cap subroutine; §5 records the resulting contract and FU-037 the one part left unguarded. Checked the CAM-SIMA impact rather than assuming it: **none** — no golden holds generated suite-cap output, no host code or compat-layer code names those symbols, and `datatable.xml` records suite/group/scheme names but no suite-cap entry points. Bumping the `ccpp_framework` submodule is the whole change for them. Swept this machine's auto-memory: nothing durable was missing — the 2026-05-15 'short Fortran symbol names' note predates and agrees with this, and the length constraint had no prior home in this file. Also merged upstream PR #782 (host-supplied Python logger in `_load_metadata_files`, Jesse Nusbaumer) — no follow-up needed, though it touches the same area as FU-025. | +| `dutchman` | 2026-09-03 | PR #780 review sweep (@peverwhee, CHANGES_REQUESTED on the `index_of_*` positive-evidence fix). Three threads: a docstring rewrite (accepted upstream), a question about `intent(out)`/`intent(inout)` on constituent indices, and an under-explained comment. Both substantive threads produced landed guards: (1) a constituent index is framework-owned, so a scheme declaring it `intent=out`/`inout` is now an error — previously `intent=out` silently became an ordinary suite var (producer and consumer agreeing with each other and both missing `%const_index`) and `intent=inout` handed the framework's own integer to a scheme that could clobber it; (2) FU-036's `index_of_` + constituent-flag guard. Verified against every `.meta` in the repo and both cam-sima trees: no host metadata trips either check. Added FU-036, which also records the rank-2/`real` metadata rule that was implemented and then **rejected by Dom** — the rationale (do not bake today's constituent storage into the parser) and the tripwire fixtures are the durable part. Corrected `briefing.md` §10 / `briefing_pm.md` §6 §8 unit-test counts: the recorded 1564 was already stale at commit `ec89dbc` (measured 1568 there), now 1571. Swept this machine's auto-memory: nothing durable was missing beyond FU-036; folded the review outcome into the existing `index_of` memory and wrote one feedback memory for the storage-assumption decision. | | `dutchman` | 2026-09-01 | Host-adoption update: NEPTUNE, CCPP-SCM and UFS have transitioned to capgen v1 on `feature/capgen-v1`; **CAM-SIMA has not — it is still on capgen v0 with v1 on testing/review branches**, so it is the remaining migration, not a fourth consumer. Added FU-034, which records Dom's decision that the `develop` merge **and** the deletion of capgen v0 + `ccpp-prebuild` happen when/after CAM-SIMA transitions — one operation, one trigger. That puts CAM-SIMA's transition on the critical path for the whole rollout, so FU-013, FU-018 and FU-030 gained critical-path annotations. Added the §3 pinning note — FU-010/FU-011 were *not* retired by adoption, they gained production consumers, while FU-012 is pinned only by CAM-SIMA branch work. Closed two long-standing unknowns in `briefing.md` §10: NEPTUNE high-altitude physics and the UFS FV3 fast-physics group both work with v1 as expected. Refreshed `briefing.md` §1/§6.3b/§10 (§10 host bullets were stale on all three models; test counts corrected 1516→1564 verified and e2e 12→13, `capgen_ng` was missing) and dropped the stale §7.1 bullet claiming `protected` is unenforced (FU-014 closed 2026-07-29). Swept this machine's auto-memory: every deferred entry already maps to an existing FU row, nothing new to fold in; wrote one new memory for the adoption fact pointing at FU-034. Later the same day, added FU-035 as fallout of the `index_of_*` positive-evidence fix: constituent auto-provisioning now requires positive evidence that a name IS a constituent, and the residual runtime-registration gap is recorded there (FU-002 would close it). | | `dutchman` | 2026-08-12 | PR #762 review-fix session (jimmielin's Claude-generated findings): added FU-033 (vertical-flip + allocatable-host → hard error). The other two findings — backward-transform temp-name collision (`suite_resolver`) and incomplete `[ccpp-table-properties]` missing `name`/`type` now erroring (`metadata_table`) — are fixed, committed, and PR'd, so they live in git, not restated here. Nothing new in this machine's auto-memory to fold beyond the above. | | `dutchman` | 2026-08-07 | folded the issue #772 / #774 session: added FU-032 (generator-local shadow follow-up). #772 shown to be a non-issue in v1 (cld_shadow e2e reproducer) and #774 detect-and-error landed in `group_cap.py` — both tracked in GitHub, not restated here | diff --git a/doc/redesign_analysis.md b/doc/redesign_analysis.md index cb8e4d7a..9ae031fc 100644 --- a/doc/redesign_analysis.md +++ b/doc/redesign_analysis.md @@ -1188,7 +1188,7 @@ subroutine ccpp___init(inst_num, ...) This injection does **not** happen for `_run`, `_timestep_init`, or `_timestep_final` unless a scheme in those phases explicitly requests `instance_number`. The suite cap's -`_physics_init` and `_physics_final` dispatch subroutines similarly pass +`suite_physics_init` and `suite_physics_final` dispatch subroutines similarly pass `instance_number` to the group cap calls when the host provides it. **Control variable validation — flat unconditional required set.** @@ -1232,7 +1232,7 @@ Symmetric to `ccpp_physics_init`'s silent skip when already `INITIALIZED`, both final-path entry points return cleanly with `errflg=0` on every repeat invocation. Three cap levels participate: -- The suite-cap `_physics_final` dispatcher silent-returns when +- The suite-cap `suite_physics_final` dispatcher silent-returns when `ccpp_suite_state` is unallocated (last-instance post-`ccpp_final` deallocation) or `ccpp_suite_state(inst_num) == CCPP_SUITE_UNREGISTERED` (any other instance post-`ccpp_final`). The `state /= FRAMEWORK_INITIALIZED` error is diff --git a/unit-tests/test_integration.py b/unit-tests/test_integration.py index 46a323f2..decbd160 100644 --- a/unit-tests/test_integration.py +++ b/unit-tests/test_integration.py @@ -449,16 +449,16 @@ def test_module_declaration(self): def test_register_subroutine_always_present(self): # _register is mandatory in the new design — emitted even when # no scheme has a register phase (state-transition skeleton only). - self.assertIn('subroutine test_simple_register', self.text) + self.assertIn('subroutine suite_register', self.text) def test_init_subroutine(self): - self.assertIn('subroutine test_simple_init', self.text) + self.assertIn('subroutine suite_init', self.text) def test_final_subroutine(self): - self.assertIn('subroutine test_simple_final', self.text) + self.assertIn('subroutine suite_final', self.text) def test_physics_run_subroutine(self): - self.assertIn('subroutine test_simple_physics_run', self.text) + self.assertIn('subroutine suite_physics_run', self.text) def test_dispatches_to_group(self): self.assertIn("case('physics')", self.text) @@ -723,7 +723,7 @@ def test_suite_init_minimal_signature(self): with open(os.path.join(self._tmpdir, 'ccpp_test_simple_cap.F90')) as fh: text = fh.read() self.assertIn( - 'subroutine test_simple_init(inst_num, ninstances, errmsg, errflg)', + 'subroutine suite_init(inst_num, ninstances, errmsg, errflg)', text, ) @@ -733,7 +733,7 @@ def test_register_passes_ninstances_to_suite_state_alloc(self): with open(os.path.join(self._tmpdir, 'ccpp_test_simple_cap.F90')) as fh: text = fh.read() self.assertIn( - 'call test_simple_suite_state_alloc(ninstances, errmsg, errflg)', + 'call suite_state_alloc(ninstances, errmsg, errflg)', text, ) @@ -790,7 +790,7 @@ def test_group_init_has_inst_num_arg(self): def test_suite_cap_dispatches_inst_num_to_group_init(self): with open(os.path.join(self._tmpdir, 'ccpp_test_simple_cap.F90')) as fh: text = fh.read() - physics_init = text.split('subroutine test_simple_physics_init')[1] + physics_init = text.split('subroutine suite_physics_init')[1] physics_init = physics_init.split('end subroutine')[0] self.assertIn('inst_num', physics_init) @@ -851,13 +851,13 @@ def test_host_cap_ccpp_final_omits_inst_num(self): def test_suite_init_omits_inst_num(self): with open(os.path.join(self._tmpdir, 'ccpp_test_simple_cap.F90')) as fh: text = fh.read() - self.assertIn('subroutine test_simple_init(errmsg, errflg)', text) + self.assertIn('subroutine suite_init(errmsg, errflg)', text) def test_register_passes_literal_one_to_state_alloc(self): with open(os.path.join(self._tmpdir, 'ccpp_test_simple_cap.F90')) as fh: text = fh.read() self.assertIn( - 'call test_simple_suite_state_alloc(1, errmsg, errflg)', text, + 'call suite_state_alloc(1, errmsg, errflg)', text, ) def test_suite_state_indexing_uses_literal_one(self): @@ -1504,19 +1504,19 @@ def tearDown(self): shutil.rmtree(self._tmpdir) def test_suite_init_calls_init_scheme(self): - sub = self.text.split('subroutine with_init_final_suite_init')[1] + sub = self.text.split('subroutine suite_init')[1] sub = sub.split('end subroutine')[0] self.assertIn('call suite_init_final_scheme_init', sub) def test_suite_final_calls_final_scheme(self): - sub = self.text.split('subroutine with_init_final_suite_final')[1] + sub = self.text.split('subroutine suite_final')[1] sub = sub.split('end subroutine')[0] self.assertIn('call suite_init_final_scheme_final', sub) def test_init_call_uses_scheme_module(self): """The scheme module is USE'd inside ``_init`` so the init subroutine is in scope.""" - sub = self.text.split('subroutine with_init_final_suite_init')[1] + sub = self.text.split('subroutine suite_init')[1] sub = sub.split('end subroutine')[0] self.assertIn( 'use suite_init_final_scheme, only:', sub, @@ -1524,7 +1524,7 @@ def test_init_call_uses_scheme_module(self): self.assertIn('suite_init_final_scheme_init', sub) def test_final_call_uses_scheme_module(self): - sub = self.text.split('subroutine with_init_final_suite_final')[1] + sub = self.text.split('subroutine suite_final')[1] sub = sub.split('end subroutine')[0] self.assertIn( 'use suite_init_final_scheme, only:', sub, @@ -1535,7 +1535,7 @@ def test_init_call_precedes_state_transition(self): """The init scheme is called BEFORE the FRAMEWORK_INITIALIZED state transition — failures during the suite-init scheme stop the state transition from firing.""" - sub = self.text.split('subroutine with_init_final_suite_init')[1] + sub = self.text.split('subroutine suite_init')[1] sub = sub.split('end subroutine')[0] call_pos = sub.index('call suite_init_final_scheme_init') state_pos = sub.index('CCPP_SUITE_FRAMEWORK_INITIALIZED') @@ -1549,7 +1549,7 @@ def test_init_call_precedes_state_transition(self): self.assertLess(call_pos, state_set) def test_final_call_precedes_unregister_transition(self): - sub = self.text.split('subroutine with_init_final_suite_final')[1] + sub = self.text.split('subroutine suite_final')[1] sub = sub.split('end subroutine')[0] call_pos = sub.index('call suite_init_final_scheme_final') state_set = sub.index( @@ -2060,24 +2060,24 @@ def test_suite_data_final_fields_subroutine_exists(self): self.assertIn('suite_data_final_fields', self._data()) def test_suite_cap_has_suite_state_alloc(self): - self.assertIn('interstitial_suite_state_alloc', self._suite_cap()) + self.assertIn('suite_state_alloc', self._suite_cap()) def test_suite_cap_register_calls_suite_state_alloc(self): # State + DDT-array allocation has moved from _init into # _register so register can be the first lifecycle entry point. text = self._suite_cap() - register_body = text.split('subroutine interstitial_register')[1].split('end subroutine')[0] - self.assertIn('interstitial_suite_state_alloc', register_body) + register_body = text.split('subroutine suite_register')[1].split('end subroutine')[0] + self.assertIn('suite_state_alloc', register_body) def test_suite_state_alloc_calls_suite_data_alloc(self): text = self._suite_cap() - alloc_body = text.split('subroutine interstitial_suite_state_alloc')[1].split('end subroutine')[0] + alloc_body = text.split('subroutine suite_state_alloc')[1].split('end subroutine')[0] self.assertIn('suite_data_alloc', alloc_body) def test_suite_init_calls_init_fields(self): # _init triggers per-instance inner allocations. text = self._suite_cap() - init_body = text.split('subroutine interstitial_init')[1].split('end subroutine')[0] + init_body = text.split('subroutine suite_init')[1].split('end subroutine')[0] self.assertIn('suite_data_init_fields', init_body) def test_suite_state_alloc_allocates_state_array(self): @@ -2085,8 +2085,8 @@ def test_suite_state_alloc_allocates_state_array(self): def test_suite_cap_final_calls_suite_state_dealloc(self): text = self._suite_cap() - final_body = text.split('subroutine interstitial_final')[1].split('end subroutine')[0] - self.assertIn('interstitial_suite_state_dealloc', final_body) + final_body = text.split('subroutine suite_final')[1].split('end subroutine')[0] + self.assertIn('suite_state_dealloc', final_body) def test_group_cap_uses_suite_data_module(self): self.assertIn('use ccpp_interstitial_data', self._group_cap()) @@ -2107,7 +2107,7 @@ def test_group_run_has_inst_num_dummy_arg(self): def test_suite_cap_passes_inst_num_to_group_run(self): """Suite cap physics_run dispatch must pass inst_num to group run.""" text = self._suite_cap() - physics_run = text.split('subroutine interstitial_physics_run')[1] + physics_run = text.split('subroutine suite_physics_run')[1] physics_run = physics_run.split('end subroutine')[0] self.assertIn('inst_num', physics_run) self.assertIn('call diag_group_run', physics_run) diff --git a/unit-tests/test_suite_cap.py b/unit-tests/test_suite_cap.py index b852d034..f1bfccb8 100644 --- a/unit-tests/test_suite_cap.py +++ b/unit-tests/test_suite_cap.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock from metadata.metadata_table import parse_metadata_file +from metadata.parse_tools import CCPPError from metadata.variable_resolver import build_flat_host_dict, SchemeStore from generator.suite_resolver import resolve_suite, ResolvedGroup from generator.suite_cap import ( @@ -37,6 +38,130 @@ def _generate(): return _generate_suite_cap('test_simple', suite_resolution, store) +# Intel truncates the mangled global name ``_mp_`` at +# 90 characters (NCAR/ccpp-framework#786). The Fortran standard caps *names* +# at 63 and says nothing about the linker symbol, so nothing but a test keeps +# the emitted symbols inside a real linker's budget. +_MANGLED_GLOBAL_LIMIT = 90 + + +def _mangled(mod_name, sub_name): + """Intel's module-procedure linker symbol for ``sub_name`` in ``mod_name``.""" + return '{}_mp_{}'.format(mod_name.lower(), sub_name.upper()) + + +class TestMangledGlobalNameLength(unittest.TestCase): + """Every emitted subroutine must fit a real linker's global-name budget. + + Regression for #786: a 29-character suite name produced a 94-character + global because the suite name appeared in BOTH the module name and the + subroutine name. Generated at the documented ceiling rather than at any + real suite's length -- the first version of this test used a 29-character + name and so missed ``_suite_state_dealloc`` (91) and + ``_register`` (which caps suite names at 34 if left prefixed). + """ + + # Longest suite name capgen supports, bounded by the host cap's + # use-rename alias ``_physics_timestep_final`` against Fortran's + # 63-char name limit rather than by the mangled global. NOTE this is + # the SUITE-CAP ceiling only: the group cap's global is + # ``ccpp___cap_mp__TIMESTEP_FINAL``, a joint + # constraint ``len(suite) + 2*len(group) <= 61``. Both hold comfortably + # today (29 + 2*12 = 53 for the longest UFS suite/group pair) but a + # 40-char suite name needs group names of 10 or fewer. + MAX_SUITE_LEN = 40 + SUITE = 'a' * MAX_SUITE_LEN + + def _text(self, suite=None): + suite_resolution, store = _resolve() + return '\n'.join(_generate_suite_cap( + suite or self.SUITE, suite_resolution, store, + _load_full_host_dict(), + )) + + @staticmethod + def _subs(text): + return [ln.split('subroutine', 1)[1].split('(')[0].strip() + for ln in text.splitlines() + if ln.strip().startswith('subroutine ')] + + def test_suite_cap_globals_fit(self): + text = self._text() + mod = 'ccpp_{}_cap'.format(self.SUITE) + subs = self._subs(text) + self.assertTrue(subs, 'no subroutines emitted') + for sub in subs: + with self.subTest(sub=sub): + self.assertLessEqual( + len(_mangled(mod, sub)), _MANGLED_GLOBAL_LIMIT, + msg='{} in {} exceeds the mangled-global budget'.format( + sub, mod), + ) + + def test_no_emitted_symbol_repeats_the_suite_name(self): + """The suite name lives in the module name; repeating it in a + subroutine name is what blew the budget.""" + for sub in self._subs(self._text()): + with self.subTest(sub=sub): + self.assertNotIn(self.SUITE, sub) + + def test_trace_and_error_strings_keep_the_suite_name(self): + """Shortening the symbol must not shorten the human-readable label: + trace lines and runtime error strings stay fully qualified.""" + text = self._text() + for phase in ('init', 'timestep_init', 'run', + 'timestep_final', 'final'): + with self.subTest(phase=phase): + self.assertIn( + "'CCPP TRACE {}_physics_{}:'".format(self.SUITE, phase), + text, + ) + for what in ('init', 'final'): + with self.subTest(sub=what): + self.assertIn( + "'CCPP TRACE {}_{}:'".format(self.SUITE, what), text) + + +class TestHostCapSuiteAliasLength(unittest.TestCase): + """The host cap use-renames each suite cap's symbols back to + ``_``; that alias is a Fortran name and is what actually + caps the supported suite-name length (#786).""" + + def _host_cap(self, suite_name): + from generator.host_cap import _generate_host_cap + suite_resolution, store = _resolve() + return '\n'.join(_generate_host_cap( + 'test_host', [suite_name], [suite_resolution], + _load_full_host_dict(), store, + )) + + def test_alias_at_the_ceiling_is_accepted(self): + suite = 'a' * TestMangledGlobalNameLength.MAX_SUITE_LEN + text = self._host_cap(suite) + self.assertIn('{}_physics_run => suite_physics_run'.format(suite), text) + + def test_alias_over_the_ceiling_raises(self): + suite = 'a' * (TestMangledGlobalNameLength.MAX_SUITE_LEN + 1) + with self.assertRaises(CCPPError) as ctx: + self._host_cap(suite) + self.assertIn('too long', str(ctx.exception)) + + +class TestSuitePhysicsGroupNameCollision(unittest.TestCase): + """A group named ``suite_physics`` would export the same symbols the + suite cap defines for its own dispatch.""" + + def test_colliding_group_name_raises(self): + for gname in ('suite_physics', 'suite'): + with self.subTest(group=gname): + suite_resolution, store = _resolve() + suite_resolution.groups[0].group_name = gname + with self.assertRaises(CCPPError) as ctx: + _generate_suite_cap('test_simple', suite_resolution, + store, _load_full_host_dict()) + self.assertIn('colliding', str(ctx.exception)) + + class TestAllSuiteSchemeNames(unittest.TestCase): def test_single_scheme(self): @@ -113,16 +238,16 @@ def test_implicit_none_private(self): def test_public_register_always_emitted(self): # _register is mandatory in the new design — always public. - self.assertIn('public :: test_simple_register', self.text) + self.assertIn('public :: suite_register', self.text) def test_public_init_final(self): - self.assertIn('public :: test_simple_init', self.text) - self.assertIn('public :: test_simple_final', self.text) + self.assertIn('public :: suite_init', self.text) + self.assertIn('public :: suite_final', self.text) def test_public_all_physics_phases(self): for phase in ('init', 'timestep_init', 'run', 'timestep_final', 'final'): self.assertIn( - 'public :: test_simple_physics_{}'.format(phase), self.text + 'public :: suite_physics_{}'.format(phase), self.text ) def test_contains_block(self): @@ -139,8 +264,8 @@ def setUp(self): self.text = '\n'.join(lines) def test_register_subroutine_present(self): - self.assertIn('subroutine test_simple_register', self.text) - self.assertIn('end subroutine test_simple_register', self.text) + self.assertIn('subroutine suite_register', self.text) + self.assertIn('end subroutine suite_register', self.text) def test_no_constituents_arg(self): # Constituents are now opt-in via type=host; not in the cap at all @@ -154,7 +279,7 @@ def test_no_scheme_register_calls(self): def test_state_alloc_called(self): # Register always allocates state (idempotent) on first call. - self.assertIn('call test_simple_suite_state_alloc', self.text) + self.assertIn('call suite_state_alloc', self.text) def test_idempotent_guard(self): # Per-instance idempotent skip if already at REGISTERED or beyond. @@ -170,12 +295,12 @@ def setUp(self): self.text = '\n'.join(_generate()) def test_init_subroutine(self): - self.assertIn('subroutine test_simple_init(errmsg, errflg)', self.text) - self.assertIn('end subroutine test_simple_init', self.text) + self.assertIn('subroutine suite_init(errmsg, errflg)', self.text) + self.assertIn('end subroutine suite_init', self.text) def test_final_subroutine(self): - self.assertIn('subroutine test_simple_final(errmsg, errflg)', self.text) - self.assertIn('end subroutine test_simple_final', self.text) + self.assertIn('subroutine suite_final(errmsg, errflg)', self.text) + self.assertIn('end subroutine suite_final', self.text) def test_init_calls_group_state_alloc(self): # No host_dict passed → single-instance → literal 1 for ninstances. @@ -188,7 +313,7 @@ def test_register_calls_suite_state_alloc(self): # Suite state allocation happens in _register, not _init. # No host_dict → single-instance → literal 1 for ninstances. self.assertIn( - 'call test_simple_suite_state_alloc(1, errmsg, errflg)', + 'call suite_state_alloc(1, errmsg, errflg)', self.text, ) @@ -203,8 +328,8 @@ def setUp(self): def test_run_dispatch_present(self): # No control vars in test setup → no-arg signature. - self.assertIn('subroutine test_simple_physics_run()', self.text) - self.assertIn('end subroutine test_simple_physics_run', self.text) + self.assertIn('subroutine suite_physics_run()', self.text) + self.assertIn('end subroutine suite_physics_run', self.text) def test_run_dispatches_to_group_cap(self): # No group_name control var → unconditional call, no select case. @@ -220,11 +345,11 @@ def test_timestep_init_dispatches_to_group_cap(self): # Group phase subroutines are always emitted so the state machine # transitions through every phase, even when no scheme has a routine # for that phase — so the dispatch must always call into the group cap. - self.assertIn('subroutine test_simple_physics_timestep_init()', self.text) + self.assertIn('subroutine suite_physics_timestep_init()', self.text) self.assertIn('call physics_timestep_init()', self.text) def test_timestep_final_dispatches_to_group_cap(self): - self.assertIn('subroutine test_simple_physics_timestep_final()', self.text) + self.assertIn('subroutine suite_physics_timestep_final()', self.text) self.assertIn('call physics_timestep_final()', self.text) def test_no_select_case_without_group_name_ctrl(self): @@ -248,9 +373,9 @@ def setUp(self): ) def _phase_block(self, phase): - sub = 'subroutine test_simple_physics_{}'.format(phase) + sub = 'subroutine suite_physics_{}'.format(phase) start = self.text.index(sub) - end = self.text.index('end subroutine test_simple_physics_{}'.format(phase), start) + end = self.text.index('end subroutine suite_physics_{}'.format(phase), start) return self.text[start:end] def test_run_dispatch_has_case_default(self): @@ -308,7 +433,7 @@ def _two_group_run_all_block(self): text = '\n'.join( _generate_suite_cap('test_simple', sr, store, _load_full_host_dict()) ) - sub = 'subroutine test_simple_physics_run' + sub = 'subroutine suite_physics_run' s = text.index(sub) e = text.index('end ' + sub, s) block = text[s:e] @@ -347,7 +472,7 @@ def test_file_content(self): content = fh.read() self.assertIn('module ccpp_test_simple_cap', content) # Register subroutine is always emitted now. - self.assertIn('subroutine test_simple_register', content) + self.assertIn('subroutine suite_register', content) self.assertTrue(content.endswith('\n')) def test_creates_output_dir(self): @@ -418,8 +543,8 @@ def test_no_index_of_X_in_suite_cap(self): self.assertNotIn('index_of_cloud_liquid_water_mixing_ratio', self.text) def test_no_const_index_call_in_init(self): - init_body = self.text.split('subroutine consume_consts_init')[1].split( - 'end subroutine consume_consts_init' + init_body = self.text.split('subroutine suite_init')[1].split( + 'end subroutine suite_init' )[0] self.assertNotIn('%const_index(', init_body) self.assertNotIn('%vars_layer', init_body) @@ -588,8 +713,8 @@ def setUpClass(cls): skip_validation=True) sr = resolve_suite(suite, store, hd) cls.text = '\n'.join(_generate_suite_cap('gasreg', sr, store, hd)) - cls.reg = cls.text.split('subroutine gasreg_register')[1].split( - 'end subroutine gasreg_register')[0] + cls.reg = cls.text.split('subroutine suite_register')[1].split( + 'end subroutine suite_register')[0] def test_call_slices_array_by_dimension(self): # The call subscript references the host dimension's local name. diff --git a/unit-tests/test_suite_resolver.py b/unit-tests/test_suite_resolver.py index 86cd98e5..a9f2ad59 100644 --- a/unit-tests/test_suite_resolver.py +++ b/unit-tests/test_suite_resolver.py @@ -3638,7 +3638,7 @@ def test_init_calls_state_alloc_with_ninstances(self): ) def test_init_subroutine_has_ninstances_arg(self): - init_sub = self.text.split('subroutine test_simple_init')[1].split('end subroutine')[0] + init_sub = self.text.split('subroutine suite_init')[1].split('end subroutine')[0] self.assertIn('ninstances', init_sub) def test_final_calls_state_dealloc(self): @@ -3674,7 +3674,7 @@ def test_init_calls_state_alloc_with_literal_1(self): ) def test_init_subroutine_has_no_ninstances_arg(self): - init_sub = self.text.split('subroutine test_simple_init')[1].split('end subroutine')[0] + init_sub = self.text.split('subroutine suite_init')[1].split('end subroutine')[0] self.assertNotIn('number_of_instances', init_sub) @@ -3753,8 +3753,8 @@ def setUp(self): ) def test_register_subroutine_emits_scheme_call(self): - register_body = self.text.split('subroutine reg_dim_register')[1].split( - 'end subroutine reg_dim_register' + register_body = self.text.split('subroutine suite_register')[1].split( + 'end subroutine suite_register' )[0] self.assertIn('call register_dim_producer_register', register_body) @@ -3767,8 +3767,8 @@ def test_register_subroutine_uses_suite_data(self): self.assertIn('use ccpp_reg_dim_data', self.text) def test_state_transitions_to_registered(self): - register_body = self.text.split('subroutine reg_dim_register')[1].split( - 'end subroutine reg_dim_register' + register_body = self.text.split('subroutine suite_register')[1].split( + 'end subroutine suite_register' )[0] self.assertIn( 'ccpp_suite_state(inst_num) = CCPP_SUITE_REGISTERED', @@ -3895,8 +3895,8 @@ def test_no_host_object_referenced(self): self.assertNotIn('%new_field', self.text) def test_register_called_once_per_scheme(self): - register_body = self.text.split('subroutine reg_consts_register')[1].split( - 'end subroutine reg_consts_register' + register_body = self.text.split('subroutine suite_register')[1].split( + 'end subroutine suite_register' )[0] # Single-pass append: each constituent scheme's _register is called # EXACTLY ONCE (the old count+copy two-pass called it twice and broke @@ -3929,8 +3929,8 @@ def test_final_tears_down_per_suite_buffer(self): # suite-cap lifecycle: filled in _register, torn down in # _final's last-to-leave block. ccpp_deallocate_dynamic_ # constituents must NOT touch it (would break re-register). - final_body = self.text.split('subroutine reg_consts_final')[1].split( - 'end subroutine reg_consts_final' + final_body = self.text.split('subroutine suite_final')[1].split( + 'end subroutine suite_final' )[0] self.assertIn( 'use ccpp_host_constituents, only: reg_consts_dynamic_constituents', @@ -3946,8 +3946,8 @@ def test_final_tears_down_per_suite_buffer(self): def test_scheme_consts_temp_declared(self): # Local temporary in the register subroutine. - register_body = self.text.split('subroutine reg_consts_register')[1].split( - 'end subroutine reg_consts_register' + register_body = self.text.split('subroutine suite_register')[1].split( + 'end subroutine suite_register' )[0] self.assertIn( 'type(ccpp_constituent_properties_t), allocatable :: scheme_consts(:)',