diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..bd6ec986 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CCPP framework — capgen v1 + +## Follow-up work: one list, tracked in git + +`doc/followups.md` is the **single source of truth** for deferred items, open +questions, and transient shims awaiting removal. + +- Add new follow-ups there. Do not start a second list in another document. +- Cite items by ID (`FU-014`). Other documents reference IDs; they must not + restate the items. +- Closed items keep their row, with the date and the reason. +- `doc/constituents_overhaul.md` is the register of record for the + constituents area and keeps its own status taxonomy; `doc/followups.md` §2 + indexes into it rather than duplicating it. + +## Memory reconciliation (this project spans several machines) + +Work on this repository happens on **more than one machine**, and each has +its own auto-memory, task list, and scratch notes. None of that travels. +`doc/followups.md` is what travels. + +**Run a reconciliation sweep when any of these is true:** + +1. The user asks for one. +2. You are about to add a new item to `doc/followups.md`. +3. This machine is missing from the reconciliation log in `doc/followups.md` + §7, or its entry is older than the newest commit touching that file. + +**The sweep:** + +1. Read every local, machine-scoped store you have access to: + - the auto-memory directory for this project (`MEMORY.md` and the + individual memory files it indexes), + - any task/todo list held by the session or the harness, + - scratch notes and `RESUME.md`-style files in working directories + outside the repository (e.g. sibling repro directories). +2. For each item found, decide what it is: + - **Durable project work** — deferred work, an open question, a decision + and its rationale, a shim-removal trigger, a known test failure, a + cross-repo dependency. These belong in `doc/followups.md`. + - **Machine-local fact** — where clones live on *this* machine, local + paths, shell/toolchain setup, personal working preferences. These stay + in auto-memory and must not be copied into the repository. + - **Session-scoped noise** — intermediate reasoning, superseded plans. + Discard. +3. For each durable item, check whether `doc/followups.md` already covers it + (match on substance, not wording). If not, add a row with a new ID, the + date it was raised, and a `file:line` or document-section pointer. If it + is covered but the local store has extra detail — a rationale, a + reproduction, a decision that was made — fold that detail in. +4. If a local memory contradicts `doc/followups.md`, the *newer* evidence + wins; correct the stale one and say which you changed. +5. Update the reconciliation log (`doc/followups.md` §7) with this machine's + hostname and the date. +6. Report what you added, folded in, or corrected. Do not silently rewrite + existing rows. + +**Do not delete auto-memory entries just because they were copied into +`doc/followups.md`** — replace the durable content with a one-line pointer to +the ID so the local store stays useful without becoming a rival list. + +## Verifying claims about original capgen + +When the question is "what did original capgen actually emit?", read +`origin/develop:test/*/*_host_integration.F90` — those files record the real +expected call lists and are the ground truth. Reasoning from an early +return in `scripts/suite_objects.py:match_variable` (or any other single +code path) is **not** proof and has produced a wrong, pushed commit +(`501d1c0`, since reverted: it claimed original capgen never put +register-phase `ccpp_constituent_properties_t` args on a call list; +`test_advection_host_integration.F90` lists them in both `test_outvars1` +and `test_reqvars1`). + +## Documentation cross-references + +Committed documents must not cite auto-memory files. Memory is per-machine, +so such a reference is dangling for every other machine and for every +reviewer. Four such references accumulated before 2026-07-28 +(`project_implementation_status.md`, `design_constituent_api.md`, +`design_constituents_mutability.md`, `design_constituent_host_wins.md`) and +none of the targets existed in this clone. Cite a committed document, a +`file:line`, or a `doc/followups.md` ID instead. diff --git a/capgen/generator/suite_resolver.py b/capgen/generator/suite_resolver.py index 30aefab6..715ee0b7 100644 --- a/capgen/generator/suite_resolver.py +++ b/capgen/generator/suite_resolver.py @@ -1709,6 +1709,19 @@ def _resolve_one_arg( # Both found — host takes precedence (suite data shouldn't duplicate host). source = 'control' if host_entry.is_control else 'host' + # A protected host variable is read-only to physics. Passing it to an + # intent(out)/intent(inout) dummy is also invalid Fortran, so without + # this the generated cap fails to compile instead. + if (host_entry is not None and host_entry.protected + and intent in ('out', 'inout')): + raise CCPPError( + "Variable '{}' (standard_name='{}') is declared intent({}) by " + "scheme '{}' phase '{}', but the host marks it protected; only " + "intent(in) is allowed for a protected variable".format( + local, std_name, intent, scheme_name, phase + ) + ) + # ---- build access expression ----------------------------------------- if host_entry is not None: # ``host_entry.access_path`` is the verbatim form from diff --git a/capgen/metadata/metadata_table.py b/capgen/metadata/metadata_table.py index 2db48f18..2fa48c6c 100644 --- a/capgen/metadata/metadata_table.py +++ b/capgen/metadata/metadata_table.py @@ -772,7 +772,7 @@ def _prop_snapshot(self) -> Dict[str, str]: # ------------------------------------------------------------------ def validate(self, require_intent: bool, context: ParseContext) -> None: - """Check that all required attributes are present. + """Check that all required attributes are present and consistent. Parameters ---------- @@ -784,7 +784,8 @@ def validate(self, require_intent: bool, context: ParseContext) -> None: Raises ------ CCPPError - If any required attribute is missing. + If a required attribute is missing, or the variable is + ``protected`` with an intent other than ``in``. """ required = {'standard_name', 'dimensions', 'type'} if require_intent: @@ -796,6 +797,11 @@ def validate(self, require_intent: bool, context: ParseContext) -> None: self.local_name, sorted(missing), context ) ) + if self.protected and self.intent not in (None, 'in'): + raise CCPPError( + "Variable '{}' is marked protected but is intent {}, " + "at {}".format(self.local_name, self.intent, context) + ) # ------------------------------------------------------------------ def __repr__(self) -> str: diff --git a/doc/briefing.md b/doc/briefing.md index 34c47057..243cc74b 100644 --- a/doc/briefing.md +++ b/doc/briefing.md @@ -253,32 +253,31 @@ control-variable arguments to the public entry points. ### 7.1 Deferred — to be resolved in upcoming work -- **Constituents overhaul.** Three reform proposals on the table - (`doc/constituents_overhaul.md`); decision pending an upcoming - meeting. Pieces involved: framework setter additions - (`set_advected`, `set_diagnostic_name`, `set_default_value`), - `is_match` relaxation, Class A vs Class B property classification. -- ~~**Validator host-metadata check.**~~ **Landed 2026-06-01**: - `ccpp_validator.py --host-files` validates `type = host` and - `type = ddt` tables against the Fortran (`doc/migration.md` §7.4). -- **Codegen-time scheme-registration cross-check.** Today's - registration check is at runtime - (`ccpp_initialize_constituents`). Stronger options: new metadata - attribute `registers_std_names = a, b, c` on register-phase - tables; cross-check at codegen. -- **Nested-subcycle `ccpp_loop_counter` semantics.** When a scheme - inside a deeply nested subcycle asks for `ccpp_loop_counter`, it - currently resolves to the **outermost** loop's counter. None of - the in-tree physics catalogs uses the inner-counter case. -- **`ccpp_datafile.py --host-files` repurpose.** The current - `--host-files` returns the generated host-API file; should be a - filtered list of *input* host metadata files (parallel to the new - `--scheme-files`). Deferred. -- **`ccpp_host_constituents.F90` suppression** when no suite touches - constituents (file is correct-but-empty under host-wins; should - not be emitted at all). -- **Python linter / formatter pass.** Pick `ruff`, apply across - `capgen/`. +Tracked in **`doc/followups.md`**, the single source of truth. This +section used to carry its own bullet list; it drifted out of step with the +parallel lists in `migration.md` §8 and `redesign_prompt.md`, and all three +were merged there on 2026-07-28. + +The headline items for a reader of this brief: + +- **Constituents overhaul** — three proposals on the table + (`doc/constituents_overhaul.md` §8); decision pending a meeting and + gating the framework setter additions. `followups.md` FU-020, FU-003. +- **Enforce `protected`** — a scheme can currently write a host variable + the host marked read-only; original capgen errored, capgen v1 does not. + FU-014. +- **Codegen-time scheme-registration cross-check** — today's check is at + runtime. FU-002. +- **Nested-subcycle `ccpp_loop_counter` semantics** — resolves to the + outermost counter. FU-001. +- **Transient shims** — `--legacy-mode`, `--gfs-dim-aliases`, + `--legacy-auto-clone-constituents`, CAM-SIMA's `capgen_compat/`, each + with an explicit removal trigger. FU-010 … FU-013. + +Landed since this section was first written: the validator host-metadata +check (FU-008, 2026-06-01). Closed as *decided against*: suppressing +`ccpp_host_constituents.F90` when unused (FU-009) — see that row for why, +and do not re-propose it. ### 7.2 Intentionally NOT supported diff --git a/doc/code_walkthrough_DRAFT.md b/doc/code_walkthrough_DRAFT.md index 7b2ab702..cce40227 100644 --- a/doc/code_walkthrough_DRAFT.md +++ b/doc/code_walkthrough_DRAFT.md @@ -392,7 +392,7 @@ Three questions prebuild developers always ask: - constituent-ness is ultimately the **host’s** decision. A scheme that only *reads* a name need not re-flag it — capgen infers it from the set of names *some* scheme flags (“rule b”). If the host declares the name as an ordinary variable, that wins - (`design_constituent_host_wins`). + (the **host-wins** rule, §8.3). **2. Where/how are constituents registered?** Exactly one way to declare a *new* one (Rule 1): a **register-phase** scheme returns an `intent=out, allocatable` array of @@ -495,7 +495,8 @@ a reliable source of the standard name. At init, the framework fills each index > **Host-wins:** if the host itself declares the `index_of_*` / framework names, the resolver > short-circuits to ordinary host-arg resolution (the constituent path is skipped). That’s the -> `design_constituent_host_wins` rule. +> **host-wins** rule, implemented by the `host_dict` short-circuit in +> `_resolve_constituent_arg` (`capgen/generator/suite_resolver.py`). ### 8.4 The whole constituent axis — `number_of_ccpp_constituents` diff --git a/doc/constituents.md b/doc/constituents.md index ad4efa58..6c291c17 100644 --- a/doc/constituents.md +++ b/doc/constituents.md @@ -666,21 +666,22 @@ framework files (listed under `` in `datatable.xml`): The host's CMake should query `ccpp_datafile.py --utility-files` to get the absolute paths to these files at the right output location. -> **These four are listed only when some suite touches constituent -> state.** `` answers "what do the generated caps need", -> which is all capgen can determine from metadata. If the *host's own -> Fortran* uses the constituent API — `use ccpp_constituent_prop_mod` -> in host code rather than only through the generated caps — then the -> host needs these modules compiled even for a suite with no -> constituents, and capgen cannot see that. Such a host must add them -> to its build itself; querying `--utility-files` alone will silently -> produce a build that fails with `Cannot open module file -> 'ccpp_constituent_prop_mod.mod'` the first time someone configures a -> constituent-free suite. +> **These four are listed unconditionally** (since 2026-07-27, FU-009). +> `ccpp_host_constituents.F90` is generated for every run — with a +> zero-size table when no suite touches constituent state — and it +> `use`s `ccpp_constituent_prop_mod`, so the framework sources are +> always a dependency of the generated caps and always appear in +> ``. > -> CAM-SIMA is exactly this case and declares them in -> `cime_config/host_framework_deps.py`. See `constituents_overhaul.md` -> §4.17 for the failure and the reasoning. +> Before that change they were scoped to suites that actually used +> constituents, on the reasoning that `` answers "what do the +> generated caps need". That broke hosts whose *own* Fortran does `use +> ccpp_constituent_prop_mod` outside the generated caps — something +> capgen cannot see from metadata — with `Cannot open module file +> 'ccpp_constituent_prop_mod.mod'` the first time someone configured a +> constituent-free suite. See `constituents_overhaul.md` §4.17, and +> §4.18 for the unresolved question of whether a host may `use` +> framework modules directly at all (FU-021). --- @@ -869,16 +870,18 @@ message naming the offending token. ### Open work items -- **Unconditional `ccpp_host_constituents.F90` emission.** The - generator currently emits `ccpp_host_constituents.F90` for every - build, even when no scheme or host actually uses the constituent - system (no `ccpp_constituent_properties_t(:)` register-phase arg, - no `is_constituent`-flagged scheme arg, no framework-named - `index_of_` / `ccpp_constituents` / etc. claimed by capgen). - When the host owns its own indices (SCM/GFS) and no scheme exercises - the constituent path, the generated file is dead code that should be - suppressed. Tracked as a deferred item; the `host_dict` precedence - rule above already keeps the file *correct* (empty) in that case. +Tracked in `doc/followups.md`; constituent-specific items are indexed in +its §2, which points into `doc/constituents_overhaul.md`. + +One correction to what this section used to say: **`ccpp_host_constituents.F90` +is emitted unconditionally, and that is deliberate** (FU-009, decided +2026-07-27). It was previously listed here as dead code to be suppressed. +The host cap re-exports this module's public API, so gating emission on +suite content would make `_ccpp_cap`'s interface expand and contract +with the suite — not something host code can compile against. With no +constituent state the module is still valid: a zero-size table, +`ccpp_number_constituents` answers 0, `ccpp_constituents_array` returns a +zero-size array, and host loops over it are no-ops. --- diff --git a/doc/constituents_overhaul.md b/doc/constituents_overhaul.md index b2c18e5b..f5e23a9e 100644 --- a/doc/constituents_overhaul.md +++ b/doc/constituents_overhaul.md @@ -964,7 +964,9 @@ construction. ## 5. Property classification (Class A vs Class B) -Proposed in `design_constituents_mutability.md` 2026-05-12. Each +Proposed 2026-05-12. (The original write-up lived in an auto-memory +design note that does not travel between machines and is no longer +available; the classification below is now the record.) Each constituent property is conceptually owned by either the scheme (physics-portable, immutable once instantiated) or the host (host-configuration, mutable post-instantiation). @@ -1284,8 +1286,10 @@ keeping.** ## 9. Appendix: framework setter inventory -(For reference during the meeting. Reproduced from -`design_constituents_mutability.md`.) +(For reference during the meeting. Originally reproduced from an +auto-memory design note that does not travel between machines; this table +is now the record. Verify against +`src/ccpp_constituent_prop_mod.F90` before relying on it.) `ccpp_constituent_properties_t` methods (`src/ccpp_constituent_prop_mod.F90`): @@ -1341,9 +1345,8 @@ setters that delegate to the underlying `ccpp_constituent_properties_t`. ## Cross-references - `doc/constituents.md` — capgen's user-facing constituents reference. -- `design_constituent_api.md` (memory) — capgen's per-instance option-A design. -- `design_constituents_mutability.md` (memory) — extended design notes incl. class A/B classification. -- `project_implementation_status.md` (memory) — current implementation state and deferred items. +- `doc/followups.md` — deferred items and open questions across the project; + §2 indexes this document's own §4 / §7 / §8. - `scripts/constituents.py` — original capgen's host-cap generator. - `src/ccpp_constituent_prop_mod.F90` — framework. - `capgen/generator/host_constituents.py` — capgen's host-side module emitter. diff --git a/doc/followups.md b/doc/followups.md new file mode 100644 index 00000000..09ed8cf0 --- /dev/null +++ b/doc/followups.md @@ -0,0 +1,160 @@ +# Follow-up work — single source of truth + +This file is the **only** list of deferred items, open questions, and +transient shims for the capgen v1 effort. It is tracked in git, so it +travels between machines; auto-memory does not. + +## How to use this file + +- **Do not start a second list.** Other documents may *reference* items by + ID (`FU-014`); they must not restate them. Three parallel lists in + `migration.md`, `briefing.md` and `redesign_prompt.md` drifted apart and + were merged here on 2026-07-28. +- **Cite by ID.** IDs are permanent and never reused. +- **Closed items keep their row**, with the date and the reason. A closed + item that is deleted gets re-proposed six months later; the reasoning is + the valuable part. See FU-009 for why this matters. +- **Detail is a pointer, not a copy.** Give `file:line` and the document + section that argues the item. Arguments live in design docs, not here. +- **Areas with their own register of record** keep it — see §2. This file + indexes into them rather than absorbing them. + +Status values: `open`, `in progress`, `blocked`, `closed`. + +--- + +## 1. Open items + +| ID | Item | Repo | Raised | Status | Detail | +|----|------|------|--------|--------|--------| +| FU-001 | `ccpp_loop_counter` inside nested subcycles resolves to the OUTERMOST loop variable | framework | 2026-05 | open | No in-tree physics catalog uses the innermost-counter case. Revisit when a real scheme needs it. | +| FU-002 | Codegen-time cross-check of scheme constituent registration | framework | 2026-05 | open | Today's check is at runtime in `ccpp_initialize_constituents`. Would need a new metadata attribute `registers_std_names = a, b, c` on register-phase tables. See `constituents_overhaul.md` §4.9. | +| FU-003 | Framework setters: `set_advected`, `set_diagnostic_name`, `set_default_value`, possibly `set_mixing_ratio_type` | framework | 2026-05 | blocked | Gated on the constituents-overhaul proposal decision (FU-020). See `constituents_overhaul.md` §4.2. | +| FU-004 | Python linter / formatter pass across `capgen/` | framework | 2026-05 | open | Pick `ruff` and apply. | +| FU-005 | Generated Fortran ↔ Codee formatter idempotency | framework | 2026-05 | open | Emitted `.F90` must round-trip cleanly through the project's Codee Fortran formatter. Highest-frequency offender is the multi-import single-line `use , only: …` — break after `only:`, one import per continued line. Worth a single pass adding a shared wrapping helper (e.g. in `generator/io_helpers.py`) called by every cap writer, rather than piecemeal; other Codee rules (space after `.not.`, …) follow once this lands. | +| FU-006 | `fortran_to_metadata` developer utility | framework | 2026-05 | open | Bootstrap a `.meta` skeleton from an existing `.F90` subroutine. | +| FU-007 | `ccpp_datafile.py` query CLI rework | framework | 2026-05-13 | open | Collapse `--host-files` / `--suite-files` / `--utility-files` into `--capgen-files`, then repurpose `--host-files` as a filtered list of **input** host metadata files (parallel to `--scheme-files`). Most hosts pack host data into a handful of files, so the filtering pay-off is small — the draw is API symmetry. | +| FU-015 | Validator: capture `protected` and `allocatable` from Fortran declarations | framework | 2026-07-28 | open | `_ArgAttrs` (`ccpp_validator.py:135`) carries only type/kind/intent/optional/rank; `_parse_decl_line:352-354` explicitly discards `protected`, `parameter` and `allocatable`. `allocatable` is the more consequential of the two — metadata declares it (`metadata_table.py:493`) and it *changes codegen* (subscript emission at call sites), so a mismatch is silently wrong output rather than a missing error. A `protected` check must accept Fortran `parameter` as satisfying it: CAM-SIMA `create_readnl_files.py:422` writes `protected = True` for namelist array dimensions that `:523` declares `integer, public, parameter`. Cost note: `_ArgAttrs` reprs appear in 7 doctests in `ccpp_validator.py`. **Deprioritised 2026-07-28** — CAM-SIMA never invokes `ccpp_validator` (no call site in `cime_config/`), so this is CI/developer value only, and FU-014 catches the same class of error where it is load-bearing. | +| FU-016 | Expose `advected` on `ResolvedArg` | framework | 2026-07-26 | open | `capgen_compat/_var_wrapper.py:~320` currently *infers* advectedness from the constituent standard-name shape (`_is_base_constituent_name`) because capgen does not surface the flag. The inference is close but not exact; exposing the real flag would make it exact. | +| FU-017 | `cime_config/host_framework_deps.py` may now be redundant | cam-sima | 2026-07-28 | open | It was added 2026-07-27 so CAM-SIMA's host code could compile `ccpp_constituent_prop_mod` in constituent-free builds. Making `ccpp_host_constituents.F90` unconditional (FU-009) put the four framework `.F90` files back into `` unconditionally, which likely covers the same ground. ~90 lines plus 8 tests plus 4 documentation sections. Verify end-to-end before the next Derecho run and remove if genuinely redundant. See `constituents_overhaul.md` §4.17. | +| FU-018 | MPAS 120km cam4 aux test fails on constituent ordering | cam-sima | 2026-07 | open | Known failure, distinct from `fadiab` (which also fails on `develop`). Analysis in `doc/cam4_fwaut_constituent_order.md`. The framework-side fix and the re-baseline decision are FU-030. Post-sign-off cleanup: strip the inert DBG-FP instrumentation (`schemes/utilities/debug_fingerprint.F90` + its call sites) from both `EXT/cam-sima-ng` and `EXT/cam-sima-ng-reference`. | +| FU-019 | Delete pushed branch `bugfix/constituents_camsima_july2026` | framework | 2026-07-27 | open | Housekeeping. The branch carried framework commit `501d1c0`, which was wrong and has been reverted; `feature/capgen-v1` is the live branch. | +| FU-024 | Confirm FU-014 Check B does not fire in a production CAM-SIMA build | cam-sima | 2026-07-29 | open | The unit tests and fixtures are green, but only a Derecho aux-test run exercises the real registry against the real suites. Risk assessed low — the three registry variables that carry `access="protected"` (`fracis`, `do_lagrangian_vertical_coordinate`, `dycore_calculates_geopotential_using_logarithms`) are all consumed `intent = in` (§5) — but `access="protected"` is not the only source: `allocatable="parameter"` also emits `protected = True` (`generate_registry_data.py:694-695`), as does `create_readnl_files.py:422` for namelist array dimensions. Fold the result back here. | +| FU-025 | Revisit capgen's logging-output scheme | framework | 2026-07-15 | open | Per-variable transform logging (`group_cap.py:_log_one_transform`, ~:545) is **temporarily emitted at WARNING** (see the `TEMPORARY level choice` comment at ~:553) purely so it shows in a default run — capgen's default level is WARNING; INFO needs `-v`. Decide its real home (INFO + `-v`, a dedicated `--report-transforms` flag [Dom's likely preference: targeted, no flood], or leave) then drop the WARNING abuse. Same pass: reclassify non-warning WARNINGs — the three shim banners (`legacy_compat.py:~85`, `dim_aliases.py`, `auto_clone_constituents.py`; fire every CAM-SIMA/SCM run) and the per-scheme "no Fortran source found … fallback" (`ccpp_capgen.py:~1162`) are informational. Flipping the default to INFO is not an option — `write_if_changed` logs per file. | +| FU-026 | `GFS_debug.F90` successor + generated debug/docs/diff utility family | framework | 2026-05-14 | open | A family of opt-in utilities generated from the resolved suite/host metadata, replacing hand-maintained duplicate state: (1) per-(suite,phase) debug scheme (min/max/mean/checksum) — the `GFS_debug.F90` successor; (2) variable provenance / first-written-last-read tracker; (3) range/NaN validator (needs new `min_value`/`max_value` attrs — overlaps constituents Class-A, FU-020); (4) per-suite reference-doc generator (Markdown first); (5) `ccpp_datafile.py diff `. Substantial CCPP-team design discussion — **do not start unilaterally**. Items 1 and 5 are the smallest first steps. Distinct from FU-006 (metadata bootstrap) and from `--no-host-introspection` (which *removes* introspection). | +| FU-027 | Consolidate emitter scoping + host-vs-suite audit (generator hardening) | framework | 2026-06-04 | open | Remaining two of a four-item hardening plan (items 1–2 done: e2e already compiles+links+runs every cap; `suite_allocate` + `constituents_dim` corpus tests landed). **#3** — one shared helper that, given a `ResolvedArg`, returns its USE-requirements and access expression, called by all four emitters (`group_cap`/`static_api`/`suite_cap`/`suite_data`); the register-USE divergence bug could not have existed if both paths shared it. **#4** — proactive walk of the four emitters reconciling host-vs-suite handling (USE / dimensions / allocatable / DDT-module / naming) in one pass. Kills the divergence bug class rather than patching instances. e2e tree off-limits without explicit permission. | +| FU-028 | CAM-SIMA schemes: undefined `intent(out)` on an early-return path | cam-sima | 2026-06-08 | open | Original capgen zero/false-initialised interstitial storage, masking schemes that leave an `intent(out)` unset on an early-return branch; capgen-ng deliberately does **not** default-init suite-owned vars, so these read heap garbage at runtime. **Decision (Dom 2026-06-08): fix each scheme in place; do NOT add suite-var default-init to capgen-ng** (that would re-mask the whole class). Expect more to surface one-by-one as suites run under capgen-ng. First instance fixed: `solar_irradiance_data_init` (set `do_spectral_scaling = .false.` before the `fixed_scon` return). Edits live in the `EXT/cam-sima-ng/src/physics/ncar_ccpp` submodule. | +| FU-029 | Decide `timestep_init` / `timestep_final` phase-call-count semantics | framework | 2026-06-10 | open | For a scheme that appears multiple times in a suite, original capgen calls its `timestep_init`/`final` **once per appearance**; capgen-ng calls it **once per group** (measured cam4: `qneg_timestep_final` 2 vs 12). Benign for cam4 (the affected phases are idempotent/guarded) but a latent b4b/correctness hazard the moment such a phase is stateful (accumulates, zeroes a buffer). CCPP intent is once-per-timestep; neither matches strictly when a scheme spans groups. Decide the intended semantics and make capgen-ng's behaviour intentional + documented. Reproduce via the standalone-capgen driver, diffing `_timestep_(init|final)` call counts. | +| FU-030 | Deterministic + documented constituent registration order in the generator | framework | 2026-06-11 | open | Root cause of the cam4 FWAUT b4b diff (the framework side of FU-018): capgen-ng registers water species alphabetically ([cloud_ice, cloud_liquid, water_vapor]) vs original's declaration order ([cloud_liquid, cloud_ice, water_vapor]), and trace gases differ too, so `air_composition`'s `thermodynamic_active_species_idx` order → `get_hydrostatic_energy` water-sum FP order → energy fixer → pervasive roundoff. Proven b4b by a flag-guarded reorder hack. **Decision (Dom): RE-BASELINE** — give capgen-ng a deterministic, documented order (qv first; an understandable rule for how constituents land in the array), then CAM-SIMA re-baselines against the original-capgen reference; not match-the-old-order. Levers: `host_constituents.py` / the legacy-auto-clone path (FU-012) / `ccpp_register_constituents` emission; intersects the constituents overhaul (FU-020). Analysis: `doc/cam4_fwaut_constituent_order.md`. | +| FU-031 | Long-term redesign of the `ccpp_static_api.F90` runtime listings | framework | 2026-05-14 | open | The suite-variable / suite-host-data listings made the introspection module ~33k lines (`-O3` effectively hangs). Immediate pressure is off — `--no-host-introspection` stubs them (→ ~800 lines) — so this is **no longer blocking**, but the long-term redesign stays open for team discussion: move the listings to a runtime read of `datatable.xml` (preferred — no recompile when listings change), or a separate `-O0` file, or static string `data` tables, or lazy-emit only the routines the host calls. Do not redesign unilaterally. | + +--- + +## 2. Constituents overhaul + +**Register of record: `doc/constituents_overhaul.md`.** That document +maintains its own status taxonomy (§4.1–4.18 marked OPEN/FIXED, §7 Q1–Q8 +open design questions, §8 the three proposals). Do not duplicate its +content here — this is a scannable index so the items are visible from the +single list. + +| ID | Item | Section | Status | +|----|------|---------|--------| +| FU-020 | **Decide between Proposal A (bugfix only) / B (class A/B split + setters) / C (host-only registration)** — gates FU-003 and several items below | §8 | blocked on meeting | +| — | Framework: `is_match` is too strict | §4.3 | open | +| — | Framework: `diag_name` portability problem | §4.4 | open | +| — | Original capgen: implicit registration | §4.5 | open (observation) | +| — | Original capgen: single-instance `ccpp_model_constituents_obj` | §4.6 | open (limitation) | +| — | Original capgen: `ConstituentVarDict` complexity | §4.7 | open (observation) | +| — | Capgen: scheme-metadata `diagnostic_name` for `is_constituent` args is host-specific | §4.10 | open | +| — | Capgen: `ccpp_scheme_utils` singleton | §4.11 | open (documented limit) | +| — | Capgen: drop `diagnostic_name_fixed`, keep only `diagnostic_name` | §4.12 | open (proposed simplification) | +| — | Capgen: error-output keyword inconsistency across emitted public API | §4.14 | open (observation) | +| — | Capgen: register-phase constituents are invisible to codegen | §4.16 | open | +| FU-021 | **May host code `use` framework modules directly, or is `_ccpp_cap` the whole contract?** Settled sub-part: adding `ccpp_constituent_prop_ptr_t` and `ccpp_constituent_properties_t` to `constituent_pub_syms` is correct regardless — 72 of CAM-SIMA's ~78 direct imports are those two types | §4.18 | open | +| — | Q1–Q8 open design questions (`default_value` class, `water_species`, `mixing_ratio_type`, post-relaxation disagreement, `%instantiate` class-B args, singleton, `_layer` suffix, constituent triplet) | §7 | open | + +`FU-020` and `FU-021` carry IDs because they are cited from outside that +document; the rest are indexed by section only. + +--- + +## 3. Transient shims awaiting removal + +Each has an explicit removal trigger. Remove the module, its unit tests, +its fixtures, and every marked touchpoint together. + +| ID | Shim | Remove when | Touchpoints | +|----|------|-------------|-------------| +| FU-010 | `--legacy-mode` | scheme metadata has migrated | `capgen/metadata/legacy_compat.py`, `unit-tests/test_legacy_compat.py`, every `# legacy-compat:` marker | +| FU-011 | `--gfs-dim-aliases` (added 2026-05-21) | GFS metadata stops spelling `vertical_layer_dimension` as `adjusted_vertical_layer_dimension_for_radiation` / `vertical_composition_dimension` | `capgen/metadata/dim_aliases.py`, `unit-tests/test_dim_aliases.py`, every `# dim-aliases:` marker | +| FU-012 | `--legacy-auto-clone-constituents` (added 2026-05-21) | consumers have moved to explicit `host_constituents(:)` declaration or register-phase scheme registration | `capgen/metadata/auto_clone_constituents.py`, `unit-tests/test_auto_clone_constituents.py`, `unit-tests/sample_files/scheme_auto_clone_consumer.meta`, `unit-tests/sample_suite_files/suite_auto_clone.xml`, every `# auto-clone-constituents:` marker | +| FU-013 | CAM-SIMA `cime_config/capgen_compat/` | phased removal plan A–G in that directory's `README.md` completes | whole directory; brief at `doc/capgen_compat_layer.md` | + +--- + +## 4. Closed + +| ID | Item | Closed | Outcome | +|----|------|--------|---------| +| FU-008 | Validator host-metadata check | 2026-06-01 | **Landed.** `ccpp_validator.py --host-files` validates `type = host` and `type = ddt` tables against module-level declarations and derived-type definitions in the `--source-files` tree. `type = control` is silent-skipped; `type = scheme` in `--host-files` is a hard error. Per-variable checks reuse `_check_arg_attributes`. See `migration.md` §7.4. | +| FU-009 | Suppress `ccpp_host_constituents.F90` when no suite touches constituent state | 2026-07-27 | **Decided against — do not re-propose.** The host cap re-exports this module's public API, so gating it on suite content would make `_ccpp_cap`'s interface expand and contract with the suite. That is not a usable API: CAM-SIMA's `cam_comp.F90` USEs six of these entry points, and its dycore coupling and analytic-IC modules use more, all compiled for every configuration. A host cannot `#ifdef` around a generator decision it cannot see, so "no constituents" must be an *answer* (zero-size table), not a missing symbol. Original capgen took the same position. Rationale in the `_generate_host_constituents` docstring, `capgen/generator/host_constituents.py`; consequences in `constituents_overhaul.md` §4.17. This item had been listed as deferred in three separate documents. | +| FU-014 | Enforce `protected`: a scheme must not write a protected host variable | 2026-07-29 | **Done**, framework commit `e68b6fb`. **A** — `protected = True` with an `intent` other than `in` rejected in `MetaVar.validate()`, `capgen/metadata/metadata_table.py:793`. **B** — scheme `intent(out\|inout)` on a protected host variable rejected in `_resolve_one_arg`, `capgen/generator/suite_resolver.py:1717`. Original capgen had both (`origin/develop:scripts/metavar.py:332`, `:415`); capgen v1 had neither, though `metadata_table.py:442` documented the rule. 7 tests added; 1555 unit tests and 13/13 end-to-end pass. Check B immediately found two real fixture bugs: `end-to-end-tests/{advection,advection_auto_clone}/test_host_data.meta` marked `test_banana_constituent_indices` protected while `test_host_data.F90:24` declares it with no `protected` attribute and `const_indices.F90:29` writes it (stray attribute removed); and the CAM-SIMA fixture in FU-023. Production confirmation is FU-024. | +| FU-023 | Fix `test_protected_reg_write_init` fixture, which violated FU-014 Check B | 2026-07-29 | **Done** (CAM-SIMA, on top of `d599908`). `protected_reg.xml` had `theta` / `potential_temperature` `access="protected"` while the shared `temp_adjust.meta` declares that standard name `intent = inout` — invalid Fortran that went unnoticed because the test compares generated text and never compiles a cap. `access="protected"` moved to `slp` / `air_pressure_at_sea_level`, which `temp_adjust.meta` reads `intent = in`; the two golden files regenerated per §6. The whole golden diff is the swap and nothing else: `protected_vars` and `initialized_vars` exchange elements, `theta` gains a `read_field` call and `slp` gains the `endrun('… is a protected variable')`, so both branches stay covered — and the read now exercises the 2-D path (`read_field(..., 'lev', ...)`) rather than the 1-D one. 160 CAM-SIMA python unit tests pass. | +| FU-022 | `_FRAMEWORK_CONST_DIM_INPUTS` cleanup | 2026-05-13 | **Done.** The hand-curated frozenset is gone; framework-constituent dimension references ride on a dedicated `used_const_dim_std_names` field on `ResolvedArg`. | + +--- + +## 5. Notes worth keeping + +**CAM-SIMA's registry does support `protected`.** Via `access="protected"` +on a `` (`src/data/generate_registry_data.py:567-570`), and via +`allocatable="parameter"` — both emit `protected = True` into the generated +`.meta` (`:694-695`). The real registry has three: `fracis` / +`fraction_of_water_insoluble_convectively_transported_species`, +`do_lagrangian_vertical_coordinate`, and +`dycore_calculates_geopotential_using_logarithms`. Every in-tree scheme +consuming them declares `intent = in`, so FU-014 Check B is not expected to +fire in a production build — but the Derecho aux tests are what prove it +(FU-024). +Namelist variables are protected too (`create_readnl_files.py:422, :440`), +read-only by construction. + +## 6. Regenerating CAM-SIMA golden test files + +`test/unit/python/test_write_init_files.py` compares generated output +byte-for-byte (`filecmp.cmp(..., shallow=False)`) against committed samples +in `test/unit/python/sample_files/write_init_files/`. There is no +`--update-golden` flag. Output is written to `test/unit/python/tmp/...`, +which is gitignored (`.gitignore:12`), so: + +```bash +python -m pytest test/unit/python/test_write_init_files.py -k +# fails on the comparison, but still writes the output files +cp test/unit/python/tmp/write_init_files/.F90 \ + test/unit/python/sample_files/write_init_files/ +git diff # review this — it is the only safeguard +python -m pytest test/unit/python/test_write_init_files.py -q +``` + +## 7. Reconciliation log + +Auto-memory, TODO lists and task lists are **per-machine** and do not travel. +Each machine records here when its local stores were last swept into this +file, per the procedure in the repository's `CLAUDE.md`. + +| Machine | Last reconciled | By | +|---------|-----------------|-----| +| `dutchman` | 2026-08-06 | first sweep of this machine; folded its auto-memory investigation notes into new rows FU-025…FU-031, added Codee `use…only:` detail to FU-005, cross-linked FU-018↔FU-030 | +| `ip-10-0-0-98.ec2.internal` | 2026-07-29 | swept on adding FU-024; local stores unchanged since the previous sweep, nothing new to fold in | +| `ip-10-0-0-98.ec2.internal` | 2026-07-28 | initial migration — merged `migration.md` §8, `briefing.md` §7.1, `redesign_prompt.md` "Still deferred", plus open items from this machine's auto-memory | + +--- + +## Cross-references + +- `doc/constituents_overhaul.md` — register of record for the constituents area (§2 above). +- `doc/migration.md` — porting guide; §8 points here. +- `doc/briefing.md` — status brief; §7.1 points here. §7.2 "Intentionally NOT supported" stays there: it is a design stance, not a work queue. +- `doc/redesign_prompt.md` — original design specification. +- `doc/capgen_compat_layer.md` — CAM-SIMA ↔ capgen compatibility layer brief (FU-013). diff --git a/doc/migration.md b/doc/migration.md index a0ccb45e..8a2f4825 100644 --- a/doc/migration.md +++ b/doc/migration.md @@ -905,21 +905,28 @@ Always generated: - `ccpp__data.meta` — inspection artifact; pairs with `ccpp__data.F90` (`.meta` ↔ `.F90` filename convention). - `datatable.xml` — build-system + host-introspection metadata. -When any scheme registers constituents: +Unconditionally, on every run: - `ccpp_host_constituents.F90` — owns `ccpp_model_constituents_obj(:)` - and the host-facing constituent API. + and the host-facing constituent API. When no suite touches constituent + state the module is still generated and still valid: the table has size + zero, `ccpp_number_constituents` answers 0, and + `ccpp_constituents_array` returns a zero-size array. - The framework's constituent sources (`ccpp_constituent_prop_mod.F90`, `ccpp_hashable.F90`, `ccpp_hash_table.F90`, `ccpp_scheme_utils.F90`) are - added to ``. **Migration note:** original capgen listed these - four on *every* run, so a host that relied on `--utility-files` alone got - them unconditionally. capgen scopes them to what the generated caps - actually need. A host whose own Fortran does `use - ccpp_constituent_prop_mod` — outside the generated caps — must now add - them to its build itself, or a constituent-free suite will fail with - `Cannot open module file 'ccpp_constituent_prop_mod.mod'`. See - `constituents.md` §6 "Framework F90 dependencies" and - `constituents_overhaul.md` §4.17. + added to ``, because the module above `use`s them. + +**Migration note (changed 2026-07-27, FU-009).** Between the start of +capgen v1 and that date, both were scoped to suites that actually used +constituents; original capgen had always emitted them unconditionally. +The scoped behaviour broke two things: a host whose own Fortran does `use +ccpp_constituent_prop_mod` outside the generated caps got `Cannot open +module file 'ccpp_constituent_prop_mod.mod'` on a constituent-free suite, +and — more fundamentally — the host cap re-exports the constituent API, +so `_ccpp_cap`'s *interface* expanded and contracted with suite +content. Host code cannot `#ifdef` around a generator decision it cannot +see. Capgen now matches original capgen here. See `constituents.md` §6 +"Framework F90 dependencies" and `constituents_overhaul.md` §4.17. ### 5.2 Per-suite data: TARGET on the instance array @@ -1274,27 +1281,30 @@ dummy arguments (scheme args and control/lifecycle variables). ## 8. Known gaps and deferred items -| Item | Status | -|--------------------------------------------|-----------------------------------------------| -| `ccpp_loop_counter` standard name inside nested subcycles | Maps to OUTERMOST loop var. None of cam-sima uses this; revisit if a scheme needs the innermost value. | -| Validator host-metadata check | **Landed 2026-06-01**: pass `--host-files`; see §7.4. | -| Constituents overhaul (Class A/B + setters) | Discussion doc at `doc/constituents_overhaul.md`. | -| Framework setters: `set_advected`, `set_diagnostic_name`, `set_default_value` | Deferred; depends on constituents-overhaul decision. | -| Codegen-time scheme-registration cross-check | Deferred; would require new `registers_std_names` metadata attr. | -| `_FRAMEWORK_CONST_DIM_INPUTS` cleanup | **Done 2026-05-13**: hand-curated frozenset gone; framework-constituent dim refs ride on a dedicated `used_const_dim_std_names` field on `ResolvedArg`. | -| Suppress `ccpp_host_constituents.F90` when unused | Deferred; currently emitted for every build even when no scheme/host actually exercises the constituent system. Now *correct* (empty) for SCM-style hosts thanks to the host-wins rule, but still dead code. See `design_constituent_host_wins.md`. | -| Python linter / formatter pass | Deferred; pick `ruff` and apply across `capgen/`. | -| Generated Fortran ↔ Codee formatter idempotency | Deferred; emitted `.F90` must round-trip cleanly through the project's Codee Fortran formatter. | -| `fortran_to_metadata` developer utility | Deferred; bootstraps a `.meta` skeleton from an existing `.F90` subroutine. | -| `--legacy-mode` shim removal | Transient; remove `metadata/legacy_compat.py`, `unit-tests/test_legacy_compat.py`, and every `# legacy-compat:` touchpoint when scheme metadata has migrated. | -| `--gfs-dim-aliases` shim removal | Transient; remove `metadata/dim_aliases.py`, `unit-tests/test_dim_aliases.py`, and every `# dim-aliases:` touchpoint when GFS metadata stops spelling `vertical_layer_dimension` as `adjusted_vertical_layer_dimension_for_radiation` / `vertical_composition_dimension`. | -| `--legacy-auto-clone-constituents` shim removal | Transient; remove `metadata/auto_clone_constituents.py`, `unit-tests/test_auto_clone_constituents.py`, sample files under `unit-tests/sample_files/scheme_auto_clone_consumer.meta` + `sample_suite_files/suite_auto_clone.xml`, and every `# auto-clone-constituents:` touchpoint when consumers have moved to explicit `host_constituents(:)` declaration or register-phase scheme registration. | -| `ccpp_datafile.py` query CLI rework | Deferred (2026-05-13); collapse `--host-files` / `--suite-files` / `--utility-files` into `--capgen-files`, then repurpose `--host-files` as a filtered list of **input** host metadata files (parallel to `--scheme-files`). Most hosts pack all host data into a handful of shared files, so the filtering pay-off is small — the draw is API symmetry. | +Tracked in **`doc/followups.md`**, the single source of truth for deferred +items, open questions and transient shims. This section used to carry its +own table; it drifted out of step with the parallel lists in +`briefing.md` §7.1 and `redesign_prompt.md`, and all three were merged on +2026-07-28. Add new items there, not here. + +Orientation: + +- **§1 Open items** — FU-001 … FU-019. +- **§2 Constituents overhaul** — index into `doc/constituents_overhaul.md`, + which remains the register of record for that area. +- **§3 Transient shims awaiting removal** — `--legacy-mode` (FU-010), + `--gfs-dim-aliases` (FU-011), `--legacy-auto-clone-constituents` (FU-012), + CAM-SIMA's `capgen_compat/` layer (FU-013), each with its removal trigger. +- **§4 Closed** — including the validator host-metadata check (FU-008, + landed 2026-06-01, see §7.4) and the decision *not* to suppress + `ccpp_host_constituents.F90` (FU-009). --- ## Cross-references +- `doc/followups.md` — deferred items, open questions and shim-removal + triggers (single source of truth; see §8). - `doc/redesign_prompt.md` — original design specification (sections marked "historic" where the implementation has evolved). - `doc/redesign_analysis.md` — analysis of the legacy ccpp-prebuild + diff --git a/doc/redesign_prompt.md b/doc/redesign_prompt.md index 5ed79126..05be2ae4 100644 --- a/doc/redesign_prompt.md +++ b/doc/redesign_prompt.md @@ -1173,8 +1173,9 @@ The following patterns from prebuild or capgen are explicitly **not** carried fo ## 18. Outstanding Work -See `MEMORY.md` (auto-memory index) and `project_implementation_status.md` -(deferred items) for the canonical list. Snapshot as of 2026-05-13: +See `doc/followups.md` for the canonical list of outstanding work. +(This previously pointed at `project_implementation_status.md` in +auto-memory, which is per-machine and does not travel.) Snapshot as of 2026-05-13: ### Landed in the 2026-05-12 session @@ -1380,56 +1381,18 @@ See `MEMORY.md` (auto-memory index) and `project_implementation_status.md` ### Still deferred -- **Constituents overhaul** — discussion doc at - `doc/constituents_overhaul.md` (2026-05-12). Three proposals on the - table (A bugfix-only / B class-A/B split + setters / C host-only - registration). Pending decision in upcoming meeting. -- **Framework setter additions** — `set_advected`, `set_diagnostic_name`, - `set_default_value`, possibly `set_mixing_ratio_type`. Coordinated with - the overhaul. -- ~~**Validator host-metadata check**~~ — **Landed 2026-06-01**: - `ccpp_validator.py --host-files` validates `type=host` and `type=ddt` - tables against module-level decls and derived-type definitions in - the same `--source-files` Fortran tree. `type=control` is silent- - skipped; `type=scheme` in `--host-files` is a hard error. Per-arg - type/kind/rank checks reuse `_check_arg_attributes`. See - `doc/migration.md` §7.4. -- **Codegen-time scheme-registration cross-check** — new metadata attr - `registers_std_names = a, b, c` on register-phase tables; replaces - current runtime `int_unassigned` check with codegen-time error. -- **Suppress `ccpp_host_constituents.F90` when unused** — currently - emitted for every build; now *correct* (empty) for SCM-style hosts - thanks to the host-wins rule, but still dead code. -- **`--legacy-mode` shim removal** — transient; remove - `metadata/legacy_compat.py`, `unit-tests/test_legacy_compat.py`, and - every `# legacy-compat:` touchpoint when scheme metadata has - migrated. -- **`--gfs-dim-aliases` shim removal** (added 2026-05-21) — - transient; remove `metadata/dim_aliases.py`, - `unit-tests/test_dim_aliases.py`, and every `# dim-aliases:` - touchpoint when GFS metadata stops spelling - `vertical_layer_dimension` as - `adjusted_vertical_layer_dimension_for_radiation` / - `vertical_composition_dimension`. -- **`--legacy-auto-clone-constituents` shim removal** (added - 2026-05-21) — transient; remove - `metadata/auto_clone_constituents.py`, - `unit-tests/test_auto_clone_constituents.py`, the sample fixtures - (`unit-tests/sample_files/scheme_auto_clone_consumer.meta`, - `unit-tests/sample_suite_files/suite_auto_clone.xml`), and every - `# auto-clone-constituents:` touchpoint when consumers have moved - to explicit `host_constituents(:)` declaration or register-phase - scheme registration. -- **Nested subcycle `ccpp_loop_counter` semantics**: a scheme inside a - nested subcycle requesting `ccpp_loop_counter` would get the - OUTERMOST counter, not the innermost. None of the cam-sima schemes - use this — revisit if a real scheme needs the innermost. -- **Python linter / formatter pass** — pick `ruff` and apply across - `capgen/`. -- **Generated Fortran ↔ Codee formatter idempotency** — emitted `.F90` - must round-trip cleanly through the project's Codee formatter. -- **`fortran_to_metadata` developer utility** — bootstrap a `.meta` - skeleton from an existing `.F90` subroutine. +Tracked in **`doc/followups.md`**, the single source of truth for deferred +items, open questions and transient shims. This section used to carry its +own bullet list; it drifted out of step with the parallel lists in +`migration.md` §8 and `briefing.md` §7.1, and all three were merged there +on 2026-07-28. Add new items to `doc/followups.md`, not here. + +Two entries that were listed here are now closed and should not be +re-proposed: the validator host-metadata check landed 2026-06-01 (FU-008), +and suppressing `ccpp_host_constituents.F90` when unused was **decided +against** 2026-07-27 (FU-009 — the host cap re-exports that module's API, +so gating it on suite content would make the host's interface expand and +contract with the suite). ### Where to find the migration summary diff --git a/end-to-end-tests/advection/test_host_data.meta b/end-to-end-tests/advection/test_host_data.meta index 960ce33e..9912bda2 100644 --- a/end-to-end-tests/advection/test_host_data.meta +++ b/end-to-end-tests/advection/test_host_data.meta @@ -57,7 +57,6 @@ long_name = Array of constituent indices units = 1 dimensions = (banana_array_dim) - protected = true type = integer [ const_index ] standard_name = test_banana_constituent_index diff --git a/end-to-end-tests/advection_auto_clone/test_host_data.meta b/end-to-end-tests/advection_auto_clone/test_host_data.meta index 960ce33e..9912bda2 100644 --- a/end-to-end-tests/advection_auto_clone/test_host_data.meta +++ b/end-to-end-tests/advection_auto_clone/test_host_data.meta @@ -57,7 +57,6 @@ long_name = Array of constituent indices units = 1 dimensions = (banana_array_dim) - protected = true type = integer [ const_index ] standard_name = test_banana_constituent_index diff --git a/unit-tests/test_metadata_table.py b/unit-tests/test_metadata_table.py index ea69c3d1..393caa14 100644 --- a/unit-tests/test_metadata_table.py +++ b/unit-tests/test_metadata_table.py @@ -376,6 +376,25 @@ def test_protected_bool(self): var = self._make_var(protected='True') self.assertTrue(var.protected) + def test_protected_with_write_intent_rejected(self): + for intent in ('out', 'inout'): + var = self._make_var(protected='True', intent=intent) + with self.assertRaises(CCPPError) as raised: + var.validate(require_intent=True, context=_ctx()) + msg = str(raised.exception) + self.assertIn('my_var', msg) + self.assertIn('protected', msg) + self.assertIn(intent, msg) + + def test_protected_with_intent_in_accepted(self): + var = self._make_var(protected='True', intent='in') + var.validate(require_intent=True, context=_ctx()) + + def test_protected_host_var_has_no_intent(self): + """Host vars carry no intent; protected must not trip the check.""" + var = self._make_var(protected='True') + var.validate(require_intent=False, context=_ctx()) + def test_optional_bool(self): var = self._make_var(optional='False') self.assertFalse(var.optional) diff --git a/unit-tests/test_suite_resolver.py b/unit-tests/test_suite_resolver.py index 44e38b03..8a506d3a 100644 --- a/unit-tests/test_suite_resolver.py +++ b/unit-tests/test_suite_resolver.py @@ -1159,6 +1159,52 @@ def test_case1_2d_array_run(self): self.assertEqual(arg.call_expr, 'gt0(lb:ub, 1:nlev)') self.assertEqual(arg.transform_case, 1) + def _protected_host_dict(self, std_name='air_temperature'): + hd = self._host_dict() + hd[std_name].protected = True + return hd + + def test_protected_host_var_write_intent_raises(self): + """A scheme may not write a host variable the host marks protected.""" + for intent in ('out', 'inout'): + hd = self._protected_host_dict() + suite_var = self._scheme_var( + 'temp', 'air_temperature', intent, 'K', + '(horizontal_dimension, vertical_layer_dimension)', + 'real', 'kind_phys') + with self.assertRaises(CCPPError) as cm: + _resolve_one_arg(suite_var, 'run', hd, {}, 'writer', set()) + msg = str(cm.exception) + self.assertIn('air_temperature', msg) + self.assertIn('protected', msg) + self.assertIn('writer', msg) + + def test_protected_host_var_intent_in_ok(self): + hd = self._protected_host_dict() + suite_var = self._scheme_var( + 'temp', 'air_temperature', 'in', 'K', + '(horizontal_dimension, vertical_layer_dimension)', + 'real', 'kind_phys') + arg = _resolve_one_arg(suite_var, 'run', hd, {}, 'reader', set()) + self.assertEqual(arg.source, 'host') + + def test_unprotected_host_var_write_intent_ok(self): + hd = self._host_dict() + suite_var = self._scheme_var( + 'temp', 'air_temperature', 'inout', 'K', + '(horizontal_dimension, vertical_layer_dimension)', + 'real', 'kind_phys') + arg = _resolve_one_arg(suite_var, 'run', hd, {}, 'writer', set()) + self.assertEqual(arg.source, 'host') + + def test_suite_owned_var_unaffected_by_protected_check(self): + """Suite-owned vars have no host entry; intent(out) stays legal.""" + hd = self._protected_host_dict() + suite_var = self._scheme_var('new_var', 'brand_new_standard_name', + 'out', 'K', '()', 'real', 'kind_phys') + arg = _resolve_one_arg(suite_var, 'run', hd, {}, 'my_scheme', set()) + self.assertEqual(arg.source, 'suite') + def test_case2_suite_owned(self): """Case 2: not in host, first use intent(out) → creates SuiteVar.""" hd = self._host_dict()