From ec14e85ca6f46abda2f5adc4b5525702750c33f1 Mon Sep 17 00:00:00 2001 From: Steve Goldhaber Date: Wed, 4 Feb 2026 15:09:59 +0100 Subject: [PATCH 1/4] Allow declaration of a scheme-dependent kind kind declaration now available in a metadata_table header. Add test to the capgen system test (test/capgen_test) --- cmake/ccpp_capgen.cmake | 15 ++- scripts/ccpp_capgen.py | 15 ++- scripts/framework_env.py | 113 +++++++++++++++--- scripts/metadata_table.py | 71 +++++++++-- scripts/parse_tools/fortran_conditional.py | 2 +- scripts/parse_tools/parse_checkers.py | 4 +- scripts/var_props.py | 8 +- test/capgen_test/CMakeLists.txt | 8 +- test/capgen_test/adjust/temp_kinds.F90 | 12 ++ test/capgen_test/capgen_test_reports.py | 1 + test/capgen_test/source_dir2/temp_set.F90 | 6 +- test/capgen_test/temp_adjust.F90 | 4 +- test/capgen_test/temp_adjust.meta | 5 +- test/capgen_test/temp_set.meta | 5 +- .../bad_kind_spec_table_properties.meta | 5 + .../duplicate_kind_spec_table_properties.meta | 6 + .../good_kind_spec_table_properties.meta | 6 + test/unit_tests/test_metadata_table.py | 42 +++++++ test/unit_tests/test_var_transforms.py | 1 - 19 files changed, 283 insertions(+), 46 deletions(-) create mode 100644 test/capgen_test/adjust/temp_kinds.F90 create mode 100644 test/unit_tests/sample_files/bad_kind_spec_table_properties.meta create mode 100644 test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta create mode 100644 test/unit_tests/sample_files/good_kind_spec_table_properties.meta diff --git a/cmake/ccpp_capgen.cmake b/cmake/ccpp_capgen.cmake index 27b8579f..0d7afb91 100644 --- a/cmake/ccpp_capgen.cmake +++ b/cmake/ccpp_capgen.cmake @@ -11,7 +11,7 @@ # SUITES - CMake list of suite xml files function(ccpp_capgen) set(optionalArgs CAPGEN_DEBUG CAPGEN_EXPECT_THROW_ERROR) - set(oneValueArgs HOST_NAME OUTPUT_ROOT VERBOSITY) + set(oneValueArgs HOST_NAME OUTPUT_ROOT VERBOSITY KIND_SPECS) set(multi_value_keywords HOSTFILES SCHEMEFILES SUITES) cmake_parse_arguments(arg "${optionalArgs}" "${oneValueArgs}" "${multi_value_keywords}" ${ARGN}) @@ -51,6 +51,19 @@ function(ccpp_capgen) separate_arguments(VERBOSE_PARAMS UNIX_COMMAND "${VERBOSE_PARAMS_SEPARATED}") list(APPEND CCPP_CAPGEN_CMD_LIST ${VERBOSE_PARAMS}) endif() + if(DEFINED arg_KIND_SPECS) + string(REPLACE "," ";" KIND_SPEC_LIST "${arg_KIND_SPECS}") + set(KIND_ARGS "") # start empty + foreach(pair IN LISTS KIND_SPEC_LIST) + # Append each pair prefixed with --kind-type and quoted. + # The surrounding double‑quotes are added explicitly so the + # resulting string contains them. + set(KIND_ARGS "${KIND_ARGS}--kind-type \"${pair}\"") + string(STRIP "${KIND_ARGS}" KIND_ARGS) + endforeach() + + list(APPEND CCPP_CAPGEN_CMD_LIST ${KIND_SPEC_PARAMS}) + endif() message(STATUS "Running ccpp_capgen.py from ${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index afd6a332..4305081f 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -125,16 +125,21 @@ def create_kinds_file(run_env, output_dir): "Create the kinds.F90 file to be used by CCPP schemes and suites" kinds_filepath = os.path.join(output_dir, KINDS_FILENAME) if run_env.logger is not None: - msg = 'Writing {} to {}' - run_env.logger.info(msg.format(KINDS_FILENAME, output_dir)) + msg = f'Writing {KINDS_FILENAME} to {output_dir}' + run_env.logger.info(msg) # end if kind_types = run_env.kind_types() with FortranWriter(kinds_filepath, "w", "kinds for CCPP", KINDS_MODULE) as kindf: for kind_type in kind_types: - use_stmt = "use ISO_FORTRAN_ENV, only: {} => {}" - kindf.write(use_stmt.format(kind_type, - run_env.kind_spec(kind_type)), 1) + kind_spec = run_env.kind_spec(kind_type) + use_stmt = f"use {run_env.kind_module(kind_type)}," + if kind_spec == kind_type: + use_stmt += f" only: {kind_type}" + else: + use_stmt += f" only: {kind_type} => {kind_spec}" + # end if + kindf.write(use_stmt, 1) # end for kindf.write_preamble() for kind_type in kind_types: diff --git a/scripts/framework_env.py b/scripts/framework_env.py index 88c9c204..582358f8 100644 --- a/scripts/framework_env.py +++ b/scripts/framework_env.py @@ -11,10 +11,14 @@ import argparse import os from parse_tools import verbose - _EPILOG = ''' ''' +## List of kinds in ISO_FORTRAN_ENV (that are useful in CCPP) +## Note: this is defined here instead of in fortran_tools to prevent a +## circular dependency +ISO_FORTRAN_KINDS = ['int8', 'int16', 'int32', 'int64', 'real32', 'real64', 'real128'] + ############################################################################### class CCPPFrameworkEnv: ############################################################################### @@ -31,6 +35,19 @@ def __init__(self, logger, ndict=None, verbose=0, clean=False, is a dict with the parsed command-line arguments (or a dictionary created with the necessary arguments). is a logger to be used by users of this object. + is a list defining the Fortran kind types which will be + public in ccpp_kinds.F90. + It has entries of the form: + kind_type=kind_specification[:kind_module] + where is a string defining the kind type name, + is the Fortran kind parameter name + (e.g., 'REAL64'), and is the (optional) Fortran + module that contains . If is + not specified, then must be a type defined in + ISO_FORTRAN_ENV. + It is allowed to have a duplicate entry for as long as + it does not specify a different type or module. + will be made available as a kind in ccpp_kinds.F90 """ emsg = '' esep = '' @@ -143,30 +160,44 @@ def __init__(self, logger, ndict=None, verbose=0, clean=False, # end if self.__generate_host_cap = self.host_name != '' self.__kind_dict = {} - if ndict and ("kind_type" in ndict): - kind_list = ndict["kind_type"] - del ndict["kind_type"] + if ndict and ("kind_types" in ndict): + kind_list = ndict["kind_types"] + del ndict["kind_types"] else: kind_list = kind_types # end if # Note that the command line uses repeated calls to 'kind_type' for kind in kind_list: kargs = [x.strip() for x in kind.strip().split('=')] + errstr = "" if len(kargs) != 2: - emsg += esep - emsg += "Error: '{}' is not a valid kind specification " - emsg += "(should be of the form =)" - emsg = emsg.format(kind) + emsg += (f"{esep}Error: '{kind}' is not a valid kind specification " + "(should be of the form =)") esep = '\n' else: kind_name, kind_spec = kargs - # Do not worry about duplicates, just use last value - self.__kind_dict[kind_name] = kind_spec + kind_specs = kind_spec.split(':') + if len(kind_specs) == 1: + errstr = self.add_kind_type(kind_name, kind_specs[0]) + elif len(kind_specs) > 2: + emsg += (f"{esep}Error: Invalid format for '{kind_name}' " + "should be [] or [, ]") + esep = '\n' + else: + errstr = self.add_kind_type(kind_name, kind_specs[0], kind_specs[1]) + # end if + if errstr: + emsg += f"{esep}{errstr}" + esep = '\n' + # end if # end if # end for + # We always need a kind_phys so add a default if necessary if "kind_phys" not in self.__kind_dict: - self.__kind_dict["kind_phys"] = "REAL64" + # Use ISO-Fortran 64-bit real + # definition for default physics kind: + self.__kind_dict['kind_phys'] = ['REAL64', 'ISO_FORTRAN_ENV'] # end if if ndict and ('use_error_obj' in ndict): self.__use_error_obj = ndict['use_error_obj'] @@ -269,16 +300,68 @@ def generate_host_cap(self): CCPPFrameworkEnv object.""" return self.__generate_host_cap + def kind_module(self, kind_type): + """Return the Fortran module that + contains the kind specification + for kind type, , + for this CCPPFrameworkEnv object. + If there is no entry for , + return None.""" + kind_mod = None + if kind_type in self.__kind_dict: + # The kind module should always be + # the second element in the list: + kind_mod = self.__kind_dict[kind_type][1] + # end if + return kind_mod + def kind_spec(self, kind_type): """Return the kind specification for kind type, for this CCPPFrameworkEnv object. If there is no entry for , return None.""" kind_spec = None if kind_type in self.__kind_dict: - kind_spec = self.__kind_dict[kind_type] + # The kind specification should always be + # the first element in the list: + kind_spec = self.__kind_dict[kind_type][0] # end if return kind_spec + def add_kind_type(self, new_ccpp_kind, new_kind, new_module=None): + """Add to our kind dictionary. + is the name of the Fortran module that defined + is the kind name as published in ccpp_kinds.f90 + This method assumes the inputs have been parsed. + Returns None or an error string if is already in the + kinds dictionary. + """ + emsg = "" + esep = "" + # Make sure we have a valid module + if new_module == None: + if new_kind.lower() in ISO_FORTRAN_KINDS: + new_module = 'ISO_FORTRAN_ENV' + else: + emsg += (f"{esep}Error: unknown kind, '{new_kind}' " + "and no Fortran module name specified") + esep = '\n' + # end if + # end if + # Check for incompatible duplicates + if ((new_ccpp_kind in self.__kind_dict) and + ((self.kind_spec(new_ccpp_kind) != new_kind) or + (self.kind_module(new_ccpp_kind) != new_module))): + emsg += (f"{esep}Error: '{new_ccpp_kind} = [{new_kind}, {new_module}]'" + f"is an invalid duplicate. {new_ccpp_kind} " + f"is already '{str(self.__kind_dict[new_ccpp_kind])}") + esep = '\n' + else: + if new_module: + self.__kind_dict[new_ccpp_kind] = [new_kind, new_module] + # end if + # end if + return emsg + def kind_types(self): """Return a list of all kind types defined in this CCPPFrameworkEnv object.""" @@ -381,12 +464,12 @@ def parse_command_line(args, description, logger=None): help='Remove files created by this script, then exit') parser.add_argument("--kind-type", type=str, action='append', - metavar="kind_type", default=list(), - help="""Data size for real() data. + metavar="kind_spec", dest="kind_types", default=list(), + help="""Data size for data (e.g., real()). Entry in the form of = e.g., --kind-type "kind_phys=REAL64" Enter more than one --kind-type entry to define multiple CCPP kinds. - SHOULD be a valid ISO_FORTRAN_ENV type""") + MUST be a valid ISO_FORTRAN_ENV type""") parser.add_argument("--generate-docfiles", metavar='HTML | Latex | HTML,Latex', type=str, diff --git a/scripts/metadata_table.py b/scripts/metadata_table.py index 5e4d2d7c..b79acb31 100755 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -15,21 +15,42 @@ A 'ccpp-table-properties' section entries are: name = : the name of the following ccpp-arg-table entries (required). - It is one of the following possibilities: + It is one of the following possibilities: - SchemeName: the name of a scheme (i.e., the name of - a scheme interface (related to SubroutineName below). + a scheme interface (related to SubroutineName below). - DerivedTypeName: a derived type name for a type which will be used - somewhere in the CCPP interface. + somewhere in the CCPP interface. - ModuleName: the name of the module whose module variables will be - used somewhere in the CCPP interface + used somewhere in the CCPP interface - HostName: the name of the host model. Variables in this section become - part of the CCPP UI, the CCPP routines called by the - host model (e.g., _ccpp_physics_run). + part of the CCPP UI, the CCPP routines called by the + host model (e.g., _ccpp_physics_run). type = : The type of header (required), one of: - scheme: A CCPP subroutine - ddt: A header for a derived data type - module: A header on some module data - host: A header on data which will be part of the CCPP UI +dependencies = : Comma-separated list of module dependencies + Each item should appear in one or more use statements in the + corresponding Fortran module +dependencies_path = : A path, relative to the location of + metadata table file, where dependencies can be found. +module_name = : only needed if module name differs from filename +source_path = +dynamic_constituent_routine = ??? : @peverwhee? +kind_spec = : One or more optional Fortran kinds defined + in the corresponding Fortran file. + The format is :[:] + - is defined in the corresponding Fortran module + - is the module name of the corresponding Fortran module + - is optional and describes the kind name + used in CCPP metadata tables and Fortran files. + These entries are added to the framework_env object and + thus to ccpp_kinds.F90 + The entries in ccpp_kinds.F90 are: + use , only + use , only => + where the first form is used if is omitted. The ccpp-arg-table section entries in this section are: name = : the name of the file object which immediately follows the @@ -63,7 +84,7 @@ type = scheme dependencies_path = dependencies = - module = # only needed if module name differs from filename + module_name = # only needed if module name differs from filename source_path = dynamic_constituent_routine = @@ -512,6 +533,42 @@ def __init_from_file(self, known_ddts, run_env, skip_ddt_check=False): self.__dependencies_path = value elif key == 'source_path': self.__fortran_src_path = os.path.join(my_dirname, value) + elif key == 'kind_spec': + # Add spec to the runtime environment's kinds dict + spec_list = [x.strip() for x in value.split(':', maxsplit=2)] + new_kind = spec_list[0] + if len(spec_list) < 2: + emsg = f"A fortran module name is required for '{new_kind}'" + self.__pobj.add_syntax_err(emsg) + continue + # end if + fort_module = spec_list[1] + if len(spec_list) > 2: + new_ccpp_kind = spec_list[2] + else: + new_ccpp_kind = new_kind + # end if + try: + check_fortran_id(new_kind, {}, True) + except CCPPError as err: + self.__pobj.add_syntax_err(f"{err}") + new_kind = None + # end try + if new_kind and (new_ccpp_kind != new_kind): + try: + check_fortran_id(new_ccpp_kind, {}, True) + except CCPPError as err: + self.__pobj.add_syntax_err(f"{err}") + new_ccpp_kind = None + # end try + # end if + if new_kind: + emsg = run_env.add_kind_type(new_ccpp_kind, + new_kind, fort_module) + if emsg: + self.__pobj.add_syntax_err(emsg) + # end if + # end if else: tok_type = "metadata table start property" self.__pobj.add_syntax_err(tok_type, token=key) diff --git a/scripts/parse_tools/fortran_conditional.py b/scripts/parse_tools/fortran_conditional.py index b1ec7eeb..17ae6859 100755 --- a/scripts/parse_tools/fortran_conditional.py +++ b/scripts/parse_tools/fortran_conditional.py @@ -10,4 +10,4 @@ FORTRAN_CONDITIONAL_REGEX_WORDS = [' ', '(', ')', '==', '/=', '<=', '>=', '<', '>', '.eqv.', '.neqv.', '.true.', '.false.', '.lt.', '.le.', '.eq.', '.ge.', '.gt.', '.ne.', '.not.', '.and.', '.or.', '.xor.'] -FORTRAN_CONDITIONAL_REGEX = re.compile(r"[\w']+|" + "|".join([word.replace('(','\(').replace(')', '\)') for word in FORTRAN_CONDITIONAL_REGEX_WORDS])) +FORTRAN_CONDITIONAL_REGEX = re.compile(r"[\w']+|" + "|".join([word.replace('(',r'\(').replace(')', r'\)') for word in FORTRAN_CONDITIONAL_REGEX_WORDS])) diff --git a/scripts/parse_tools/parse_checkers.py b/scripts/parse_tools/parse_checkers.py index 5aaaaeab..9a688a13 100755 --- a/scripts/parse_tools/parse_checkers.py +++ b/scripts/parse_tools/parse_checkers.py @@ -13,13 +13,13 @@ ######################################################################## _UNITLESS_REGEX = "1" -_NON_LEADING_ZERO_NUM = "[1-9]\d*" +_NON_LEADING_ZERO_NUM = r"[1-9]\d*" _CHAR_WITH_UNDERSCORE = "([a-zA-Z]+_[a-zA-Z]+)+" _NEGATIVE_NON_LEADING_ZERO_NUM = f"[-]{_NON_LEADING_ZERO_NUM}" _POSITIVE_NON_LEADING_ZERO_NUM = f"[+]{_NON_LEADING_ZERO_NUM}" _UNIT_EXPONENT = f"({_NEGATIVE_NON_LEADING_ZERO_NUM}|{_POSITIVE_NON_LEADING_ZERO_NUM}|{_NON_LEADING_ZERO_NUM})" _UNIT_REGEX = f"[a-zA-Z]+{_UNIT_EXPONENT}?" -_UNITS_REGEX = f"^({_CHAR_WITH_UNDERSCORE}|{_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" +_UNITS_REGEX = rf"^({_CHAR_WITH_UNDERSCORE}|{_UNIT_REGEX}(\s{_UNIT_REGEX})*|{_UNITLESS_REGEX})$" _UNITS_RE = re.compile(_UNITS_REGEX) _MAX_MOLAR_MASS = 10000.0 diff --git a/scripts/var_props.py b/scripts/var_props.py index 66434e7c..15c5e3bc 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -835,7 +835,7 @@ class VarCompatObj: 'scheme_files':'', \ 'suites':''}, \ kind_types=["kind_phys=REAL64", \ - "kind_dyn=REAL32", \ + "kind_dyn=REAL32", \ "kind_host=REAL64"]) >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", [], "var1_lname", False,\ "var_stdname", "real", "kind_phys", "m", [], "var2_lname", False,\ @@ -1172,7 +1172,7 @@ def _get_kind_convstrs(self, var1_kind, var2_kind, run_env): 'scheme_files':'', \ 'suites':''}, \ kind_types=["kind_phys=REAL64", \ - "kind_dyn=REAL32", \ + "kind_dyn=REAL32", \ "kind_host=REAL64"]) >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') @@ -1227,7 +1227,7 @@ def _get_unit_convstrs(self, var1_units, var2_units): 'scheme_files':'', \ 'suites':''}, \ kind_types=["kind_phys=REAL64", \ - "kind_dyn=REAL32", \ + "kind_dyn=REAL32", \ "kind_host=REAL64"]) >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') @@ -1318,7 +1318,7 @@ def _get_dim_transforms(self, var1_dims, var2_dims): 'scheme_files':'', \ 'suites':''}, \ kind_types=["kind_phys=REAL64", \ - "kind_dyn=REAL32", \ + "kind_dyn=REAL32", \ "kind_host=REAL64"]) >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90') diff --git a/test/capgen_test/CMakeLists.txt b/test/capgen_test/CMakeLists.txt index 48ad6097..8288569f 100644 --- a/test/capgen_test/CMakeLists.txt +++ b/test/capgen_test/CMakeLists.txt @@ -6,9 +6,10 @@ # #------------------------------------------------------------------------------ set(SCHEME_FILES "setup_coeffs" "temp_set" "temp_adjust" "temp_calc_adjust") -set(SUITE_SCHEME_FILES "make_ddt" "environ_conditions") +set(SUITE_SCHEME_FILES "make_ddt" "environ_conditions" "temp_kinds") set(HOST_FILES "test_host_data" "test_host_mod") set(SUITE_FILES "ddt_suite.xml" "temp_suite.xml") +set(KIND_TYPE "kind_phys=REAL64") # HOST is the name of the executable we will build. # We assume there are files ${HOST}.meta and ${HOST}.F90 in CMAKE_SOURCE_DIR @@ -38,10 +39,12 @@ foreach(sfile ${SUITE_SCHEME_FILES}) find_file(fort_file "${sfile}.F90" NO_CACHE HINTS ${CMAKE_CURRENT_SOURCE_DIR} HINTS ${CMAKE_CURRENT_SOURCE_DIR}/source_dir1 - HINTS ${CMAKE_CURRENT_SOURCE_DIR}/source_dir2) + HINTS ${CMAKE_CURRENT_SOURCE_DIR}/source_dir2 + HINTS ${CMAKE_CURRENT_SOURCE_DIR}/adjust) list(APPEND SUITE_SCHEME_FORTRAN_FILES ${fort_file}) unset(fort_file) endforeach() + list(TRANSFORM SUITE_SCHEME_FILES APPEND ".meta" OUTPUT_VARIABLE SUITE_SCHEME_META_FILES) list(TRANSFORM HOST_FILES APPEND ".F90" OUTPUT_VARIABLE CAPGEN_HOST_FORTRAN_FILES) list(TRANSFORM HOST_FILES APPEND ".meta" OUTPUT_VARIABLE CAPGEN_HOST_METADATA_FILES) @@ -54,6 +57,7 @@ ccpp_capgen(CAPGEN_DEBUG ON SCHEMEFILES ${SCHEME_META_FILES} ${SUITE_SCHEME_META_FILES} SUITES ${SUITE_FILES} HOST_NAME ${HOST} + KIND_TYPES ${KIND_TYPES} OUTPUT_ROOT "${CCPP_CAP_FILES}") # Retrieve the list of Fortran files required for test host from datatable.xml and set to CCPP_CAPS_LIST diff --git a/test/capgen_test/adjust/temp_kinds.F90 b/test/capgen_test/adjust/temp_kinds.F90 new file mode 100644 index 00000000..59e813e5 --- /dev/null +++ b/test/capgen_test/adjust/temp_kinds.F90 @@ -0,0 +1,12 @@ +! Define a new Fortran kind for use within +! various temp_* test files. + +module temp_kinds + + implicit none + private + + integer, public, parameter :: temp_r8 = selected_real_kind(12) !8-byte real + integer, public, parameter :: temp_i8 = selected_int_kind (13) !8-byte integer + +end module temp_kinds diff --git a/test/capgen_test/capgen_test_reports.py b/test/capgen_test/capgen_test_reports.py index 292c1a65..3c683aab 100644 --- a/test/capgen_test/capgen_test_reports.py +++ b/test/capgen_test/capgen_test_reports.py @@ -37,6 +37,7 @@ os.path.join(_BUILD_DIR, "ccpp", "ccpp_ddt_suite_cap.F90"), os.path.join(_BUILD_DIR, "ccpp", "ccpp_temp_suite_cap.F90")] _DEPENDENCIES = [os.path.join(_TEST_DIR, "adjust", "qux.F90"), + os.path.join(_TEST_DIR, "adjust", "temp_kinds.F90"), os.path.join(_TEST_DIR, "ddt2"), os.path.join(_TEST_DIR, "bar.F90"), os.path.join(_TEST_DIR, "foo.F90")] diff --git a/test/capgen_test/source_dir2/temp_set.F90 b/test/capgen_test/source_dir2/temp_set.F90 index 760fbf25..0a0aa92c 100644 --- a/test/capgen_test/source_dir2/temp_set.F90 +++ b/test/capgen_test/source_dir2/temp_set.F90 @@ -3,7 +3,7 @@ MODULE temp_set - USE ccpp_kinds, ONLY: kind_phys + USE ccpp_kinds, ONLY: kind_phys, kind_temp IMPLICIT NONE PRIVATE @@ -32,7 +32,7 @@ SUBROUTINE temp_set_run(ncol, lev, timestep, temp_level, temp_diag, temp, ps, & real(kind_phys), intent(inout) :: temp_diag(:,:) real(kind_phys), intent(inout) :: soil_levs(slev_lbound:) real(kind_phys), intent(inout) :: var_array(:,:,:,:) - real(kind_phys), intent(out) :: to_promote(:, :) + real(kind_temp), intent(out) :: to_promote(:, :) real(kind_phys), intent(out) :: promote_pcnst(:) character(len=512), intent(out) :: errmsg integer, intent(out) :: errflg @@ -62,7 +62,7 @@ SUBROUTINE temp_set_run(ncol, lev, timestep, temp_level, temp_diag, temp, ps, & var_array(:,:,:,:) = 1._kind_phys - ! + ! internal_scalar_var = soil_levs(slev_lbound) internal_scalar_var = soil_levs(0) diff --git a/test/capgen_test/temp_adjust.F90 b/test/capgen_test/temp_adjust.F90 index 4dcf408f..b39baf61 100644 --- a/test/capgen_test/temp_adjust.F90 +++ b/test/capgen_test/temp_adjust.F90 @@ -3,7 +3,7 @@ MODULE temp_adjust - USE ccpp_kinds, ONLY: kind_phys + USE ccpp_kinds, ONLY: kind_phys, kind_temp IMPLICIT NONE PRIVATE @@ -43,7 +43,7 @@ subroutine temp_adjust_run(foo, timestep, interstitial_var, temp_prev, temp_laye real(kind_phys), intent(inout) :: ps(:) REAL(kind_phys), intent(in) :: temp_prev(:) REAL(kind_phys), intent(inout) :: temp_layer(foo) - real(kind_phys), intent(in) :: to_promote(:) + real(kind_temp), intent(in) :: to_promote(:) real(kind_phys), intent(in) :: promote_pcnst(:) integer, intent(out) :: interstitial_var(:) character(len=512), intent(out) :: errmsg diff --git a/test/capgen_test/temp_adjust.meta b/test/capgen_test/temp_adjust.meta index 963f87ac..89756872 100644 --- a/test/capgen_test/temp_adjust.meta +++ b/test/capgen_test/temp_adjust.meta @@ -1,7 +1,8 @@ [ccpp-table-properties] name = temp_adjust type = scheme - dependencies = qux.F90 + kind_spec = temp_r8:temp_kinds:kind_temp + dependencies = qux.F90, temp_kinds.F90 dependencies_path = adjust [ccpp-arg-table] name = temp_adjust_register @@ -87,7 +88,7 @@ units = K dimensions = (horizontal_loop_extent) type = real - kind = kind_phys + kind = kind_temp intent = in [ promote_pcnst ] standard_name = promote_this_variable_with_no_horizontal_dimension diff --git a/test/capgen_test/temp_set.meta b/test/capgen_test/temp_set.meta index ecd06985..545d8a22 100644 --- a/test/capgen_test/temp_set.meta +++ b/test/capgen_test/temp_set.meta @@ -2,6 +2,9 @@ name = temp_set type = scheme source_path = source_dir2 + kind_spec = temp_r8:temp_kinds:kind_temp + dependencies = temp_kinds.F90 + dependencies_path = adjust [ccpp-arg-table] name = temp_set_run type = scheme @@ -60,7 +63,7 @@ units = K dimensions = (horizontal_loop_extent, vertical_layer_dimension) type = real - kind = kind_phys + kind = kind_temp intent = out [ promote_pcnst ] standard_name = promote_this_variable_with_no_horizontal_dimension diff --git a/test/unit_tests/sample_files/bad_kind_spec_table_properties.meta b/test/unit_tests/sample_files/bad_kind_spec_table_properties.meta new file mode 100644 index 00000000..d51a30d4 --- /dev/null +++ b/test/unit_tests/sample_files/bad_kind_spec_table_properties.meta @@ -0,0 +1,5 @@ +[ccpp-table-properties] + name = bad_scheme + type = scheme + dependencies = + kind_spec = temp_r8 diff --git a/test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta b/test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta new file mode 100644 index 00000000..71abfe5a --- /dev/null +++ b/test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta @@ -0,0 +1,6 @@ +[ccpp-table-properties] + name = scheme_scheme + type = scheme + dependencies = fmodule.F90 + kind_spec = temp_r8:fmodule:kind_temp + kind_spec = kind_temp:fmodule diff --git a/test/unit_tests/sample_files/good_kind_spec_table_properties.meta b/test/unit_tests/sample_files/good_kind_spec_table_properties.meta new file mode 100644 index 00000000..31a0ec33 --- /dev/null +++ b/test/unit_tests/sample_files/good_kind_spec_table_properties.meta @@ -0,0 +1,6 @@ +[ccpp-table-properties] + name = good_scheme + type = scheme + dependencies = fmodule.F90 + kind_spec = temp_r8:fmodule:kind_temp + kind_spec = temp_i8:fmodule diff --git a/test/unit_tests/test_metadata_table.py b/test/unit_tests/test_metadata_table.py index c09b040c..0190322e 100644 --- a/test/unit_tests/test_metadata_table.py +++ b/test/unit_tests/test_metadata_table.py @@ -399,5 +399,47 @@ def test_invalid_table_properties_type(self): emsg = "Invalid metadata table type, 'banana', at " self.assertTrue(emsg in str(context.exception)) + def test_added_kind_spec(self): + """Test that adding a kind_spec to a Metadata table works as expected""" + known_ddts = list() + filename = os.path.join(SAMPLE_FILES_DIR, + "good_kind_spec_table_properties.meta") + + # Test that we can parse the table with no error + _ = parse_metadata_file(filename, known_ddts, self._DUMMY_RUN_ENV) + # Test that the expected names are in the table + kind_types = self._DUMMY_RUN_ENV.kind_types() + self.assertEqual(len(kind_types), 3) + self.assertIn("kind_temp", kind_types) + self.assertEqual(self._DUMMY_RUN_ENV.kind_module("kind_temp"), "fmodule") + self.assertEqual(self._DUMMY_RUN_ENV.kind_spec("kind_temp"), "temp_r8") + self.assertIn("temp_i8", kind_types) + self.assertIn("kind_phys", kind_types) + + def test_bad_kind_spec(self): + """Test that adding a bad kind_spec to a Metadata table returns expected error""" + known_ddts = list() + filename = os.path.join(SAMPLE_FILES_DIR, + "bad_kind_spec_table_properties.meta") + + with self.assertRaises(Exception) as context: + _ = parse_metadata_file(filename, known_ddts, self._DUMMY_RUN_ENV) + + emsg = "A fortran module name is required for 'temp_r8'" + self.assertTrue(emsg in str(context.exception), msg=str(context.exception)) + + def test_duplicate_kind_spec(self): + """Test that adding a duplicate kind_spec to a Metadata table returns expected error""" + known_ddts = list() + filename = os.path.join(SAMPLE_FILES_DIR, + "duplicate_kind_spec_table_properties.meta") + + with self.assertRaises(Exception) as context: + _ = parse_metadata_file(filename, known_ddts, self._DUMMY_RUN_ENV) + + emsg = ("'kind_temp = [kind_temp, fmodule]'is an invalid duplicate. " + "kind_temp is already '['temp_r8', 'fmodule'],") + self.assertTrue(emsg in str(context.exception), msg=str(context.exception)) + if __name__ == "__main__": unittest.main() diff --git a/test/unit_tests/test_var_transforms.py b/test/unit_tests/test_var_transforms.py index 7a2592b3..3d5e3477 100644 --- a/test/unit_tests/test_var_transforms.py +++ b/test/unit_tests/test_var_transforms.py @@ -473,4 +473,3 @@ def test_incompatible_tendency_variable(self): if __name__ == "__main__": unittest.main() - From 4d8b04d7799877624e16835e1dcee1af610e1609 Mon Sep 17 00:00:00 2001 From: Steve Goldhaber Date: Fri, 6 Feb 2026 16:10:52 +0100 Subject: [PATCH 2/4] Modify syntax for declaring new kind - Modified the syntax for new-kind declaration as suggested by @climbfuji - Reverted string formatting for log message --- scripts/ccpp_capgen.py | 4 ++-- scripts/metadata_table.py | 23 +++++++++++-------- test/capgen_test/temp_adjust.meta | 2 +- test/capgen_test/temp_set.meta | 2 +- .../duplicate_kind_spec_table_properties.meta | 4 ++-- .../good_kind_spec_table_properties.meta | 4 ++-- test/unit_tests/test_metadata_table.py | 2 +- 7 files changed, 22 insertions(+), 19 deletions(-) diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index 4305081f..e14e4542 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -125,8 +125,8 @@ def create_kinds_file(run_env, output_dir): "Create the kinds.F90 file to be used by CCPP schemes and suites" kinds_filepath = os.path.join(output_dir, KINDS_FILENAME) if run_env.logger is not None: - msg = f'Writing {KINDS_FILENAME} to {output_dir}' - run_env.logger.info(msg) + msg = 'Writing {} to {}' + run_env.logger.info(msg.format(KINDS_FILENAME, output_dir)) # end if kind_types = run_env.kind_types() with FortranWriter(kinds_filepath, "w", diff --git a/scripts/metadata_table.py b/scripts/metadata_table.py index b79acb31..b657dd2c 100755 --- a/scripts/metadata_table.py +++ b/scripts/metadata_table.py @@ -40,11 +40,12 @@ dynamic_constituent_routine = ??? : @peverwhee? kind_spec = : One or more optional Fortran kinds defined in the corresponding Fortran file. - The format is :[:] - - is defined in the corresponding Fortran module + The format is fortran_module:ccpp_kind_name=>kind_name or + fortran_module:kind_name - is the module name of the corresponding Fortran module - - is optional and describes the kind name - used in CCPP metadata tables and Fortran files. + - is defined in the corresponding Fortran module + - is optional and describes the kind name used in CCPP + metadata tables and Fortran files. These entries are added to the framework_env object and thus to ccpp_kinds.F90 The entries in ccpp_kinds.F90 are: @@ -536,17 +537,19 @@ def __init_from_file(self, known_ddts, run_env, skip_ddt_check=False): elif key == 'kind_spec': # Add spec to the runtime environment's kinds dict spec_list = [x.strip() for x in value.split(':', maxsplit=2)] - new_kind = spec_list[0] + fort_module = spec_list[0] if len(spec_list) < 2: - emsg = f"A fortran module name is required for '{new_kind}'" + emsg = f"A Fortran kind name is required for '{value}'" self.__pobj.add_syntax_err(emsg) continue # end if - fort_module = spec_list[1] - if len(spec_list) > 2: - new_ccpp_kind = spec_list[2] + new_ccpp_kind = spec_list[1] + spec_list = [x.strip() for x in new_ccpp_kind.split('=>', maxsplit=1)] + if len(spec_list) > 1: + new_kind = spec_list[1] + new_ccpp_kind = spec_list[0] else: - new_ccpp_kind = new_kind + new_kind = new_ccpp_kind # end if try: check_fortran_id(new_kind, {}, True) diff --git a/test/capgen_test/temp_adjust.meta b/test/capgen_test/temp_adjust.meta index 89756872..8acb05ce 100644 --- a/test/capgen_test/temp_adjust.meta +++ b/test/capgen_test/temp_adjust.meta @@ -1,7 +1,7 @@ [ccpp-table-properties] name = temp_adjust type = scheme - kind_spec = temp_r8:temp_kinds:kind_temp + kind_spec = temp_kinds:kind_temp=>temp_r8 dependencies = qux.F90, temp_kinds.F90 dependencies_path = adjust [ccpp-arg-table] diff --git a/test/capgen_test/temp_set.meta b/test/capgen_test/temp_set.meta index 545d8a22..42bbb194 100644 --- a/test/capgen_test/temp_set.meta +++ b/test/capgen_test/temp_set.meta @@ -2,7 +2,7 @@ name = temp_set type = scheme source_path = source_dir2 - kind_spec = temp_r8:temp_kinds:kind_temp + kind_spec = temp_kinds:kind_temp=>temp_r8 dependencies = temp_kinds.F90 dependencies_path = adjust [ccpp-arg-table] diff --git a/test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta b/test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta index 71abfe5a..12c1cfc5 100644 --- a/test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta +++ b/test/unit_tests/sample_files/duplicate_kind_spec_table_properties.meta @@ -2,5 +2,5 @@ name = scheme_scheme type = scheme dependencies = fmodule.F90 - kind_spec = temp_r8:fmodule:kind_temp - kind_spec = kind_temp:fmodule + kind_spec = fmodule:kind_temp=>temp_r8 + kind_spec = fmodule:kind_temp diff --git a/test/unit_tests/sample_files/good_kind_spec_table_properties.meta b/test/unit_tests/sample_files/good_kind_spec_table_properties.meta index 31a0ec33..67aeef86 100644 --- a/test/unit_tests/sample_files/good_kind_spec_table_properties.meta +++ b/test/unit_tests/sample_files/good_kind_spec_table_properties.meta @@ -2,5 +2,5 @@ name = good_scheme type = scheme dependencies = fmodule.F90 - kind_spec = temp_r8:fmodule:kind_temp - kind_spec = temp_i8:fmodule + kind_spec = fmodule:kind_temp=>temp_r8 + kind_spec = fmodule:temp_i8 diff --git a/test/unit_tests/test_metadata_table.py b/test/unit_tests/test_metadata_table.py index 0190322e..74f598d0 100644 --- a/test/unit_tests/test_metadata_table.py +++ b/test/unit_tests/test_metadata_table.py @@ -425,7 +425,7 @@ def test_bad_kind_spec(self): with self.assertRaises(Exception) as context: _ = parse_metadata_file(filename, known_ddts, self._DUMMY_RUN_ENV) - emsg = "A fortran module name is required for 'temp_r8'" + emsg = "A Fortran kind name is required for 'temp_r8'" self.assertTrue(emsg in str(context.exception), msg=str(context.exception)) def test_duplicate_kind_spec(self): From 26fa65512979178d032e00cfcb0a9ba7e1fd8958 Mon Sep 17 00:00:00 2001 From: goldy <1588651+gold2718@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:51:15 +0100 Subject: [PATCH 3/4] Apply suggestion from @climbfuji Co-authored-by: Dom Heinzeller --- scripts/var_props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/var_props.py b/scripts/var_props.py index 15c5e3bc..ba5d9445 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -835,7 +835,7 @@ class VarCompatObj: 'scheme_files':'', \ 'suites':''}, \ kind_types=["kind_phys=REAL64", \ - "kind_dyn=REAL32", \ + "kind_dyn=REAL32", \ "kind_host=REAL64"]) >>> VarCompatObj("var_stdname", "real", "kind_phys", "m", [], "var1_lname", False,\ "var_stdname", "real", "kind_phys", "m", [], "var2_lname", False,\ From b46566191d25ed0902555765a87e03ac7a2b8015 Mon Sep 17 00:00:00 2001 From: goldy <1588651+gold2718@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:53:24 +0100 Subject: [PATCH 4/4] Apply suggestion from @climbfuji Co-authored-by: Dom Heinzeller --- scripts/var_props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/var_props.py b/scripts/var_props.py index ba5d9445..d732ae5e 100755 --- a/scripts/var_props.py +++ b/scripts/var_props.py @@ -1227,7 +1227,7 @@ def _get_unit_convstrs(self, var1_units, var2_units): 'scheme_files':'', \ 'suites':''}, \ kind_types=["kind_phys=REAL64", \ - "kind_dyn=REAL32", \ + "kind_dyn=REAL32", \ "kind_host=REAL64"]) >>> _DOCTEST_CONTEXT1 = ParseContext(linenum=3, filename='foo.F90') >>> _DOCTEST_CONTEXT2 = ParseContext(linenum=5, filename='bar.F90')