Skip to content

Add to_file To Generated Fortran Namelist Types #53

Description

@MuellerSeb

Generated namelist types can currently be populated through set(...) or
from_file(...), queried through is_set(...), and checked through
is_valid(...). They cannot write their current state back to a Fortran
namelist file.

Fortran provides intrinsic namelist output, but directly applying it to the
generated storage is not sufficient for this API:

  • a namelist group has a fixed set of output variables and cannot omit an
    optional field dynamically,
  • valid generated objects may still contain internal NaN, NUL, or integer
    sentinels for unset optional values,
  • an allocatable namelist variable must be allocated when the namelist write is
    executed, so an unallocated value cannot represent an omitted field,
  • flexible-tail arrays must not expose their unused sentinel-padded capacity,
  • intrinsic namelist formatting, especially for strings and exceptional real
    values, differs between processors, and
  • intrinsic derived-array output is not guaranteed to use the explicit
    component syntax understood consistently by the supported tooling.

Add a generated to_file(...) type-bound procedure backed by a deliberately
narrow field-by-field writer. The writer should generate portable namelist
input for the schema subset supported by nml-tools rather than serializing
the internal sentinel representation.

Public Interface

Each generated namelist type should expose:

integer function to_file(this, file, mode, errmsg) result(status)
  class(nml_config_t), intent(in), target :: this
  character(len=*), intent(in) :: file
  character(len=*), intent(in), optional :: mode
  character(len=*), intent(out), optional :: errmsg
end function to_file

Example usage:

status = cfg%to_file("config.nml", errmsg=errmsg)
status = cfg%to_file("combined.nml", mode="append", errmsg=errmsg)

mode is case-insensitive and supports:

  • replace, the default: replace the complete destination file with this
    namelist group;
  • append: append this group to a missing file or a file containing other
    groups.

Any other mode returns NML_ERR_INVALID_MODE without modifying the destination.
Reserve to_file as a generated type member so a schema property cannot collide
with it case-insensitively.

Generated helper status codes should add:

  • NML_ERR_WRITE for serialization or formatted-write failures,
  • NML_ERR_NML_EXISTS when append mode finds the same group already present,
  • NML_ERR_INVALID_MODE for an unsupported mode value.

Continue using the existing open and close error codes for failures at those
stages.

Validation And File Modes

to_file(...) must call this%is_valid(...) before inspecting, creating,
truncating, or appending to the destination. Propagate the validation status and
message unchanged when validation fails.

In replace mode, open the formatted sequential file for writing with replacement
semantics only after validation succeeds.

In append mode:

  1. If the file exists, scan it before opening it for append.
  2. Match a namelist group header as an exact Fortran identifier,
    case-insensitively. A comment containing &config or a group such as
    &config_extra must not match config.
  3. Return NML_ERR_NML_EXISTS without changing the file if the group already
    exists.
  4. Otherwise create the file when absent or append the new group after existing
    content.

Append mode does not replace or merge an existing group. Updating one group in
place while preserving arbitrary surrounding text is a separate feature.

Every generated write should use iostat and iomsg. On failure, attempt to
close an opened unit while preserving the primary write error. A close failure
after otherwise successful output should return the existing close error.

Serialization Policy

Write the group header, zero or more field assignments, and the terminating
slash using formatted sequential output. Do not use an intrinsic namelist write
for the complete group.

Emit fields deterministically in resolved schema properties order. A valid
group in which every optional value is unset is written as an empty group:

&config
/

Defaults are concrete values and are written. Unset optional values are omitted
according to the existing generated presence/sentinel rules. Required values
have already been checked by is_valid().

Use these scalar representations:

  • integers: I0 formatting;
  • reals: G0 formatting with point-decimal output;
  • logicals: explicit .true. or .false. spelling;
  • strings: double-quoted values with embedded double quotes doubled.

For fixed-length strings, omit trailing storage padding while preserving leading
and interior blanks. Reject a concretely set string containing NUL, carriage
return, or line feed with NML_ERR_WRITE; those characters cannot be represented
portably by the generated one-record assignment form. The exact NUL sentinel for
an unset string is omitted rather than rejected.

Treat an optional NaN sentinel as unset. Reject positive or negative infinity
with NML_ERR_WRITE, because standard namelist input has no portable infinity
literal. Ordinary finite real values should never depend on a processor-specific
NaN or Infinity spelling.

Intrinsic Array Writer

Add generic array-writing procedures to the generated helper module for the
intrinsic types and kinds used by generated namelist modules. The helper should
receive:

  • the output unit,
  • the schema/write name,
  • a two-dimensional view of the complete backing storage,
  • the backing-storage shape,
  • the shape that should be emitted,
  • an optional two-dimensional logical presence mask, and
  • error status/message output.

The generated to_file(...) procedure may pointer-remap each complete,
contiguous array field to a two-dimensional view:

  • the first dimension remains the row dimension,
  • all remaining dimensions are flattened into a column dimension in Fortran
    element order,
  • rank-one arrays therefore have exactly one column.

Declaring this with TARGET permits the temporary pointer association for the
duration of to_file(...). Do not leave an allocatable namelist variable
unallocated as an omission mechanism.

Shape Handling

For ordinary fixed-shape and runtime-sized arrays, the emitted shape is the
current complete array shape. Runtime arrays therefore use the dimensions
already selected through set_dims(...), not the configured defaults.

For flexible-tail arrays, call filled_shape(...) and pass the resulting shape
as the emitted shape. Keep the complete storage shape separately. This is
necessary because a multidimensional filled prefix is not always a contiguous
prefix of the flattened backing storage.

For example, backing shape [3, 5, 4] and filled shape [3, 2, 2] should emit:

values(:,1,1) = ...
values(:,2,1) = ...
values(:,1,2) = ...
values(:,2,2) = ...

The helper must derive each backing-storage column from the multidimensional
subscripts and both shapes; it must not treat those four output columns as the
first four flattened storage columns. If the first dimension is itself flexible,
restrict the first-dimension section to its filled extent.

Namelist output does not encode rank or runtime extents. A different generated
instance must call set_dims(...) with compatible dimensions before
from_file(...) reads a file containing non-default runtime extents.

Column And Mask Formatting

Emit at most one assignment record per used column, preserving the existing
generated convention of no spaces after commas in subscripts:

values(:,1) = 1, 2, 3
values(:,2) = 4, 5, 6

The optional presence mask uses .true. for concrete values and .false. for
unset elements. Represent internal missing elements with standard namelist null
values:

values(:,1) = 1,,3

Skip an all-missing column. Stop after its last present element so trailing
missing storage is left unchanged by from_file(...) without emitting
sentinels. A valid flex-tail prefix contains no internal missing elements, but
the same helper should support sparse optional fixed arrays.

Derived Values

Write derived values with explicit component assignments. This is clearer than
positional buffers, avoids dependence on imported type declaration order, and
is compatible with f90nml.

Scalar example:

set%flag = .true.
set%value = 1

Array example:

sets(1)%flag = .true.
sets(1)%value = 1
sets(2)%flag = .false.
sets(2)%value = 2

For higher-rank derived arrays, emit complete explicit subscripts in Fortran
element order, followed by components in resolved schema order. Omit unset
optional components individually. Do not emit component array sections such as
sets(:)%flag; current f90nml does not handle that form reliably.

Acceptance Criteria

  • Generated types expose to_file(...) with replace and append modes.
  • Invalid objects and invalid modes leave existing destinations unchanged.
  • Replace mode writes exactly one complete group after validation succeeds.
  • Append mode creates missing files, appends after unrelated groups, and rejects
    an exact case-insensitive duplicate without changing the file.
  • Open, serialization, write, and close failures return targeted status codes and
    iomsg-based diagnostics.
  • Scalars use deterministic portable formatting, including quoted strings with
    doubled embedded quotes.
  • Unset optional scalar and derived-component sentinels are omitted.
  • NUL/CR/LF strings and infinite real values are rejected with targeted errors.
  • Fixed and runtime arrays are written in full columns in Fortran order.
  • Sparse optional arrays use namelist nulls without writing sentinel values.
  • Flex-tail arrays write only their validated filled rectangular shape.
  • Runtime-sized output uses the instance's current allocated shape.
  • Scalar and array derived values use explicit component syntax accepted by both
    generated Fortran input and f90nml.
  • to_file is included in generated-member collision checks.
  • README API documentation, generated fixtures, and representative examples are
    updated with the new behavior and runtime-dimension round-trip requirement.

Tests

Add focused generator and runtime coverage for:

  • valid and invalid objects, including an empty all-optional group;
  • default replace behavior and case-insensitive mode selection;
  • append creation, append after unrelated groups, exact duplicate detection,
    comment/prefix non-matches, and invalid modes;
  • open, write, and close error propagation;
  • integers, finite reals, logicals, empty/blank strings, embedded quotes, and
    rejected NUL/CR/LF strings and infinities;
  • omitted optional scalar values and optional derived components;
  • rank-one, multidimensional, fixed, runtime-sized, sparse, and flex-tail arrays;
  • column ordering, subscript spelling, null masks, all-missing columns, and
    filled-shape trimming across multiple flexible dimensions;
  • non-default runtime dimensions, including a successful round trip after a
    matching set_dims(...) call;
  • explicit local/imported derived scalars and arrays, parsed by both generated
    from_file(...) code and f90nml;
  • case-insensitive schema collision rejection for a property named to_file;
  • updated golden fixtures and generated example artifacts.

Out Of Scope

  • f2py and generated Python to_file(...) wrappers.
  • CLI or TUI save behavior.
  • Replacing or merging one existing group inside a multi-group file.
  • Automatically reconstructing runtime dimensions during from_file(...).
  • Preserving comments, whitespace, or original formatting.
  • Atomic rollback after a partial external write or append failure.
  • General serialization of unsupported nested derived values, derived
    components that are arrays, or other schema features outside the current
    generated Fortran model.

Activity

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions