Skip to content

Commit

Permalink
Add optional attribute to Capgen (#529)
Browse files Browse the repository at this point in the history
This PR adds support to use the fortran optional attribute at the scheme level in Capgen. This functionality does not replace the active attribute, but rather builds on it.

Previously, conditionally allocated fields needed by a scheme were required to be accompanied with the allocation logic defined by the host (e.g the "active" condition). With this change, the scheme can internally query the "presence" instead of carrying the host model logic "down into the scheme".

The following modifications were made:

Metadata parsing: Check for compatibility between scheme and host metadata. If host has inactive variable, ensure that scheme has variable declared as optional.
Fortran parsing: Check for consistency between metadata and fortran file. If fortran file does not contain the optional attribute in its declaration, report error.
For any optional scheme variable, add optional attribute to cap level declarations.
Declare null pointer at cap level when host does not use optional scheme variable
  • Loading branch information
dustinswales authored Mar 8, 2024
1 parent ff28d15 commit c546cca
Show file tree
Hide file tree
Showing 16 changed files with 417 additions and 71 deletions.
11 changes: 11 additions & 0 deletions scripts/ccpp_capgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,17 @@ def compare_fheader_to_mheader(meta_header, fort_header, logger):
lname = mvar.get_prop_value('local_name')
arrayref = is_arrayspec(lname)
fvar, find = find_var_in_list(lname, flist)
# Check for consistency between optional variables in metadata and
# optional variables in fortran. Error if optional attribute is
# missing from fortran declaration.
mopt = mvar.get_prop_value('optional')
if find and mopt:
fopt = fvar.get_prop_value('optional')
if (not fopt):
errmsg = 'Missing optional attribute in fortran declaration for variable {}, in file {}'
errors_found = add_error(errors_found, errmsg.format(mname,title))
# end if
# end if
if mind >= flen:
if arrayref:
# Array reference, variable not in Fortran table
Expand Down
2 changes: 1 addition & 1 deletion scripts/ccpp_datafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ def _new_var_entry(parent, var, full_entry=True):
"diagnostic_name", "diagnostic_name_fixed",
"kind", "persistence", "polymorphic", "protected",
"state_variable", "type", "units", "molar_mass",
"advected", "top_at_one"])
"advected", "top_at_one", "optional"])
prop_list.extend(Var.constituent_property_names())
# end if
ventry = ET.SubElement(parent, "var")
Expand Down
2 changes: 1 addition & 1 deletion scripts/fortran_tools/parse_fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ def parse_fortran_var_decl(line, source, run_env):
>>> parse_fortran_var_decl("integer :: foo = 0", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name')
'foo'
>>> parse_fortran_var_decl("integer :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('optional')
False
>>> parse_fortran_var_decl("integer, optional :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('optional')
'True'
>>> parse_fortran_var_decl("integer, dimension(:) :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions')
Expand Down
55 changes: 39 additions & 16 deletions scripts/metavar.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ class Var:
>>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'ino'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ParseSyntaxError: Invalid intent variable property, 'ino', at <standard input>:1
>>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in', 'optional' : 'false'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ParseSyntaxError: Invalid variable property name, 'optional', at <standard input>:1
>>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm s-1', 'dimensions' : '()', 'type' : 'real', 'intent' : 'in', 'optional' : 'false'}, ParseSource('vname', 'SCHEME', ParseContext()), _MVAR_DUMMY_RUN_ENV) #doctest: +ELLIPSIS
<metavar.Var hi_mom: foo at 0x...>
# Check that two variables that differ in their units - m vs km - are compatible
>>> Var({'local_name' : 'foo', 'standard_name' : 'hi_mom', 'units' : 'm', \
'dimensions' : '()', 'type' : 'real', 'intent' : 'in'}, \
Expand Down Expand Up @@ -207,6 +207,8 @@ class Var:
default_in=False),
VariableProperty('top_at_one', bool, optional_in=True,
default_in=False),
VariableProperty('optional', bool, optional_in=True,
default_in=False),
VariableProperty('target', bool, optional_in=True,
default_in=False)]

Expand Down Expand Up @@ -1025,7 +1027,7 @@ def conditional(self, vdicts):
vars_needed.append(dvar)
return (conditional, vars_needed)

def write_def(self, outfile, indent, wdict, allocatable=False,
def write_def(self, outfile, indent, wdict, allocatable=False, target=False,
dummy=False, add_intent=None, extra_space=0, public=False):
"""Write the definition line for the variable to <outfile>.
If <dummy> is True, include the variable's intent.
Expand Down Expand Up @@ -1077,23 +1079,30 @@ def write_def(self, outfile, indent, wdict, allocatable=False,
raise CCPPError(errmsg.format(name))
# end if
# end if
optional = self.get_prop_value('optional')
if protected and dummy:
intent_str = 'intent(in) '
elif allocatable:
if dimstr or polymorphic:
intent_str = 'allocatable '
intent_str = 'allocatable '
if target:
intent_str = 'allocatable,'
intent_str += ' target'
else:
intent_str = ' '*13
# end if
elif intent is not None:
alloval = self.get_prop_value('allocatable')
if (intent.lower()[-3:] == 'out') and alloval:
intent_str = f"allocatable, intent({intent})"
elif optional:
intent_str = f"intent({intent}),{' '*(5 - len(intent))}"
intent_str += 'target, optional '
else:
intent_str = f"intent({intent}){' '*(5 - len(intent))}"
# end if
elif not dummy:
intent_str = ''
intent_str = ' '*20
else:
intent_str = ' '*13
# end if
Expand All @@ -1111,26 +1120,40 @@ def write_def(self, outfile, indent, wdict, allocatable=False,
extra_space -= len(targ)
if self.is_ddt():
if polymorphic:
dstr = "class({kind}){cspc}{intent} :: {name}{dims} ! {sname}"
cspc = comma + ' '*(extra_space + 12 - len(kind))
dstr = "class({kind}){cspace}{intent} :: {name}{dims}"
cspace = comma + ' '*(extra_space + 12 - len(kind))
else:
dstr = "type({kind}){cspc}{intent} :: {name}{dims} ! {sname}"
cspc = comma + ' '*(extra_space + 13 - len(kind))
dstr = "type({kind}){cspace}{intent} :: {name}{dims}"
cspace = comma + ' '*(extra_space + 13 - len(kind))
# end if
else:
if kind:
dstr = "{type}({kind}){cspc}{intent} :: {name}{dims} ! {sname}"
cspc = comma + ' '*(extra_space + 17 - len(vtype) - len(kind))
dstr = "{type}({kind}){cspace}{intent} :: {name}{dims}"
cspace = comma + ' '*(extra_space + 17 - len(vtype) - len(kind))
else:
dstr = "{type}{cspc}{intent} :: {name}{dims} ! {sname}"
cspc = comma + ' '*(extra_space + 19 - len(vtype))
dstr = "{type}{cspace}{intent} :: {name}{dims}"
cspace = comma + ' '*(extra_space + 19 - len(vtype))
# end if
# end if

outfile.write(dstr.format(type=vtype, kind=kind, intent=intent_str,
name=name, dims=dimstr, cspc=cspc,
name=name, dims=dimstr, cspace=cspace,
sname=stdname), indent)

def write_ptr_def(self, outfile, indent, name, kind, dimstr, vtype, extra_space=0):
"""Write the definition line for local null pointer declaration to <outfile>."""
comma = ', '
if kind:
dstr = "{type}({kind}){cspace}pointer :: {name}{dims}{cspace2} => null()"
cspace = comma + ' '*(extra_space + 20 - len(vtype) - len(kind))
cspace2 = ' '*(20 -len(name) - len(dimstr))
else:
dstr = "{type}{cspace}pointer :: {name}{dims}{cspace2} => null()"
cspace = comma + ' '*(extra_space + 22 - len(vtype))
cspace2 = ' '*(20 -len(name) - len(dimstr))
# end if
outfile.write(dstr.format(type=vtype, kind=kind, name=name, dims=dimstr,
cspace=cspace, cspace2=cspace2), indent)

def is_ddt(self):
"""Return True iff <self> is a DDT type."""
return not self.__intrinsic
Expand Down
Loading

0 comments on commit c546cca

Please sign in to comment.