• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MuellerSeb / nml-tools / 26450441458

26 May 2026 01:19PM UTC coverage: 80.914% (+1.6%) from 79.274%
26450441458

Pull #31

github

web-flow
Merge 159294b4b into ec67687b1
Pull Request #31: Add Reusable Schema Definitions With `$defs` And `$ref`

385 of 458 new or added lines in 6 files covered. (84.06%)

2 existing lines in 1 file now uncovered.

3027 of 3741 relevant lines covered (80.91%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.21
/src/nml_tools/codegen_fortran.py
1
"""Fortran code generation."""
2

3
from __future__ import annotations
1✔
4

5
import math
1✔
6
from dataclasses import dataclass
1✔
7
from pathlib import Path
1✔
8
from typing import Any, Iterable, cast
1✔
9

10
from jinja2 import Environment, FileSystemLoader, StrictUndefined
1✔
11

12
from ._utils import (
1✔
13
    FORTRAN_IDENTIFIER,
14
    normalize_constant_values,
15
    normalize_runtime_dimensions,
16
    reject_constant_dimension_overlap,
17
    strip_trailing_whitespace,
18
)
19
from .validate import validate_schema_defaults
1✔
20

21
_TEMPLATE_ENV = Environment(
1✔
22
    loader=FileSystemLoader(Path(__file__).resolve().parent / "templates"),
23
    trim_blocks=True,
24
    lstrip_blocks=False,
25
    keep_trailing_newline=True,
26
    undefined=StrictUndefined,
27
)
28

29

30
@dataclass
1✔
31
class ScalarTypeInfo:
1✔
32
    """Information about a scalar Fortran type."""
33

34
    type_spec: str
1✔
35
    arg_type_spec: str
1✔
36
    kind: str | None
1✔
37
    category: str
1✔
38
    length_expr: str | None = None
1✔
39

40

41
@dataclass
1✔
42
class FieldTypeInfo:
1✔
43
    """Information about a field (scalar or array) Fortran type."""
44

45
    type_spec: str
1✔
46
    arg_type_spec: str
1✔
47
    dimensions: list[str]
1✔
48
    kind: str | None
1✔
49
    category: str
1✔
50
    length_expr: str | None = None
1✔
51
    element_category: str | None = None
1✔
52

53

54
@dataclass
1✔
55
class FieldSpec:
1✔
56
    """Information required to render a schema property."""
57

58
    order: int
1✔
59
    name: str
1✔
60
    title: str
1✔
61
    description: str | None
1✔
62
    declaration: str
1✔
63
    local_declaration: str
1✔
64
    required: bool
1✔
65
    sentinel_assignment: str | None
1✔
66
    sentinel_check: str | None
1✔
67
    default_assignment: str | None
1✔
68
    set_default_assignment: str | None
1✔
69
    set_present_assignment: str | None
1✔
70
    argument_declaration: str
1✔
71
    type_category: str
1✔
72
    runtime_sized_array: bool = False
1✔
73
    rank: int = 0
1✔
74

75

76
@dataclass
1✔
77
class ArrayDefaultSpec:
1✔
78
    """Normalized representation of an array default value."""
79

80
    source_values: list[Any]
1✔
81
    pad_values: list[Any] | None
1✔
82
    order_values: list[int] | None
1✔
83

84

85
@dataclass
1✔
86
class ConstantSpec:
1✔
87
    """Constant definition for helper modules."""
88

89
    name: str
1✔
90
    type_spec: str
1✔
91
    value: str
1✔
92
    doc: str | None
1✔
93

94

95
def generate_fortran(
1✔
96
    schema: dict[str, Any],
97
    output: str | Path,
98
    *,
99
    helper_module: str = "nml_helper",
100
    kind_module: str | None = None,
101
    kind_map: dict[str, str] | None = None,
102
    kind_allowlist: Iterable[str] | None = None,
103
    constants: dict[str, int] | None = None,
104
    dimensions: dict[str, int] | None = None,
105
    module_doc: str | None = None,
106
    f2py_handle_helpers: bool = False,
107
) -> None:
108
    """Generate a Fortran module from *schema* at *output*."""
109
    output_path = Path(output)
1✔
110
    rendered = render_fortran(
1✔
111
        schema,
112
        file_name=output_path.name,
113
        helper_module=helper_module,
114
        kind_module=kind_module,
115
        kind_map=kind_map,
116
        kind_allowlist=kind_allowlist,
117
        constants=constants,
118
        dimensions=dimensions,
119
        module_doc=module_doc,
120
        f2py_handle_helpers=f2py_handle_helpers,
121
    )
122
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
123
    output_path.write_text(rendered, encoding="ascii")
1✔
124

125

126
def render_fortran(
1✔
127
    schema: dict[str, Any],
128
    *,
129
    file_name: str,
130
    helper_module: str = "nml_helper",
131
    kind_module: str | None = None,
132
    kind_map: dict[str, str] | None = None,
133
    kind_allowlist: Iterable[str] | None = None,
134
    constants: dict[str, int] | None = None,
135
    dimensions: dict[str, int] | None = None,
136
    module_doc: str | None = None,
137
    f2py_handle_helpers: bool = False,
138
) -> str:
139
    """Render a Fortran module from *schema*."""
140
    context = _build_context(
1✔
141
        schema,
142
        helper_module=helper_module,
143
        kind_module=kind_module,
144
        kind_map=kind_map,
145
        kind_allowlist=kind_allowlist,
146
        constants=constants,
147
        dimensions=dimensions,
148
        module_doc=module_doc,
149
        f2py_handle_helpers=f2py_handle_helpers,
150
    )
151
    context["file_name"] = file_name
1✔
152
    rendered = _TEMPLATE_ENV.get_template("fortran_module.f90.j2").render(context)
1✔
153
    return strip_trailing_whitespace(rendered)
1✔
154

155

156
def generate_helper(
1✔
157
    output: str | Path,
158
    *,
159
    module_name: str = "nml_helper",
160
    len_buf: int = 1024,
161
    constants: list[ConstantSpec] | None = None,
162
    module_doc: str | None = None,
163
    helper_header: str | None = None,
164
) -> None:
165
    """Generate the helper Fortran module at *output*."""
166
    output_path = Path(output)
×
167
    rendered = render_helper(
×
168
        file_name=output_path.name,
169
        module_name=module_name,
170
        len_buf=len_buf,
171
        constants=constants,
172
        module_doc=module_doc,
173
        helper_header=helper_header,
174
    )
175
    output_path.parent.mkdir(parents=True, exist_ok=True)
×
176
    output_path.write_text(rendered, encoding="ascii")
×
177

178

179
def render_helper(
1✔
180
    *,
181
    file_name: str,
182
    module_name: str = "nml_helper",
183
    len_buf: int = 1024,
184
    constants: list[ConstantSpec] | None = None,
185
    module_doc: str | None = None,
186
    helper_header: str | None = None,
187
) -> str:
188
    """Render the helper Fortran module."""
189
    if not module_name:
1✔
190
        raise ValueError("helper module name must be a non-empty string")
×
191
    if len_buf <= 0:
1✔
192
        raise ValueError("helper len_buf must be positive")
×
193
    return _TEMPLATE_ENV.get_template("nml_helper.f90.j2").render(
1✔
194
        {
195
            "file_name": file_name,
196
            "module_name": module_name,
197
            "len_buf": len_buf,
198
            "constants": constants or [],
199
            "module_doc": module_doc,
200
            "helper_header": helper_header,
201
        }
202
    )
203

204

205
def _build_context(
1✔
206
    schema: dict[str, Any],
207
    *,
208
    helper_module: str,
209
    kind_module: str | None,
210
    kind_map: dict[str, str] | None,
211
    kind_allowlist: Iterable[str] | None,
212
    constants: dict[str, int] | None,
213
    dimensions: dict[str, int] | None = None,
214
    module_doc: str | None = None,
215
    f2py_handle_helpers: bool = False,
216
) -> dict[str, Any]:
217
    if not helper_module:
1✔
218
        raise ValueError("helper module name must be a non-empty string")
×
219
    if "x-fortran-kind-module" in schema:
1✔
220
        raise ValueError("schema must not define 'x-fortran-kind-module'")
×
221
    namelist_name = schema.get("x-fortran-namelist")
1✔
222
    if not isinstance(namelist_name, str):
1✔
223
        raise ValueError("schema must define 'x-fortran-namelist'")
×
224

225
    if schema.get("type") != "object":
1✔
226
        raise ValueError("schema root must be of type 'object'")
×
227

228
    properties = schema.get("properties")
1✔
229
    if not isinstance(properties, dict) or not properties:
1✔
230
        raise ValueError("schema must define object 'properties'")
×
231

232
    property_items: list[tuple[str, str, dict[str, Any]]] = []
1✔
233
    property_name_map: dict[str, str] = {}
1✔
234
    for prop_name, prop in properties.items():
1✔
235
        if not isinstance(prop_name, str) or not prop_name.strip():
1✔
236
            raise ValueError("property names must be non-empty strings")
×
237
        key = prop_name.lower()
1✔
238
        if key in property_name_map:
1✔
239
            raise ValueError(
1✔
240
                "property names must be unique (case-insensitive): "
241
                f"'{property_name_map[key]}' and '{prop_name}'"
242
            )
243
        property_name_map[key] = prop_name
1✔
244
        if not isinstance(prop, dict):
1✔
245
            raise ValueError(f"property '{prop_name}' must be an object")
×
246
        property_items.append((prop_name, key, prop))
1✔
247

248
    required_fields_raw = _ordered_unique(schema.get("required", []))
1✔
249
    required_fields: list[str] = []
1✔
250
    for req_name in required_fields_raw:
1✔
251
        if not isinstance(req_name, str):
1✔
252
            raise ValueError("schema 'required' entries must be strings")
×
253
        req_key = req_name.lower()
1✔
254
        if req_key not in property_name_map:
1✔
255
            raise ValueError(f"required property '{req_name}' is not defined")
1✔
256
        if req_key not in required_fields:
1✔
257
            required_fields.append(req_key)
1✔
258
    required_set = set(required_fields)
1✔
259
    reserved_property_default_names: set[str] = set()
1✔
260
    for _, attr_name, prop in property_items:
1✔
261
        has_default_parameter = False
1✔
262
        if prop.get("type") == "array":
1✔
263
            has_default_parameter = _array_default_value(prop) is not None
1✔
264
        else:
265
            has_default_parameter = "default" in prop
1✔
266
        if has_default_parameter:
1✔
267
            reserved_property_default_names.add(f"{attr_name}_default".lower())
1✔
268

269
    module_name = f"nml_{namelist_name}"
1✔
270
    type_name = f"{module_name}_t"
1✔
271
    doc_class = f"{module_name}_t"
1✔
272
    brief_text = schema.get("title", namelist_name)
1✔
273
    details_text = schema.get("description", brief_text)
1✔
274

275
    fields: list[FieldSpec] = []
1✔
276
    sentinel_assignments: list[str] = []
1✔
277
    default_assignments: list[str] = []
1✔
278
    set_optional_defaults: list[str] = []
1✔
279
    local_init_assignments: list[str] = []
1✔
280
    set_required_assignments: list[str] = []
1✔
281
    presence_cases: list[dict[str, Any]] = []
1✔
282
    required_scalar_names: set[str] = set()
1✔
283
    required_array_by_name: dict[str, dict[str, Any]] = {}
1✔
284
    flex_bound_vars: set[str] = set()
1✔
285
    flex_arrays: list[dict[str, Any]] = []
1✔
286
    default_parameters: list[str] = []
1✔
287
    enum_parameters: list[str] = []
1✔
288
    enum_functions: list[dict[str, Any]] = []
1✔
289
    enum_checks: list[dict[str, Any]] = []
1✔
290
    bounds_parameters: list[str] = []
1✔
291
    bounds_functions: list[dict[str, Any]] = []
1✔
292
    bounds_checks: list[dict[str, Any]] = []
1✔
293
    static_constants = normalize_constant_values(constants)
1✔
294
    runtime_dimension_values = normalize_runtime_dimensions(dimensions)
1✔
295
    reject_constant_dimension_overlap(static_constants, runtime_dimension_values)
1✔
296
    validate_schema_defaults(
1✔
297
        schema,
298
        constants=static_constants,
299
        dimensions=runtime_dimension_values,
300
    )
301
    shape_constants: dict[str, int] = {**static_constants, **runtime_dimension_values}
1✔
302
    runtime_dimensions: list[dict[str, str]] = []
1✔
303
    runtime_dimension_locals: dict[str, str] = {}
1✔
304
    runtime_default_extent_requirements: list[dict[str, Any]] = []
1✔
305
    runtime_allocations: list[str] = []
1✔
306
    runtime_deallocations: list[str] = []
1✔
307
    runtime_local_allocations: list[str] = []
1✔
308
    kind_ids: list[str] = []
1✔
309
    requires_ieee = False
1✔
310
    uses_partly_set = False
1✔
311
    helper_imports = [
1✔
312
        "nml_file_t",
313
        "nml_line_buffer",
314
        "NML_OK",
315
        "NML_ERR_FILE_NOT_FOUND",
316
        "NML_ERR_OPEN",
317
        "NML_ERR_NOT_OPEN",
318
        "NML_ERR_NML_NOT_FOUND",
319
        "NML_ERR_READ",
320
        "NML_ERR_CLOSE",
321
        "NML_ERR_REQUIRED",
322
        "NML_ERR_ENUM",
323
        "NML_ERR_BOUNDS",
324
        "NML_ERR_NOT_SET",
325
        "NML_ERR_INVALID_NAME",
326
        "NML_ERR_INVALID_INDEX",
327
        "idx_check",
328
        "to_lower",
329
    ]
330
    if f2py_handle_helpers:
1✔
331
        helper_imports.append("NML_ERR_INVALID_HANDLE")
1✔
332

333
    def _helper_import_local_name(import_spec: str) -> str:
1✔
334
        if "=>" in import_spec:
1✔
335
            return import_spec.split("=>", 1)[0].strip()
×
336
        return import_spec.strip()
1✔
337

338
    def _helper_import_local_names() -> set[str]:
1✔
339
        return {_helper_import_local_name(existing).lower() for existing in helper_imports}
1✔
340

341
    def _add_helper_import(name: str) -> None:
1✔
342
        if not any(existing.lower() == name.lower() for existing in helper_imports):
1✔
343
            helper_imports.append(name)
1✔
344

345
    def _unique_generated_name(base_name: str, taken_names: set[str]) -> str:
1✔
346
        if base_name.lower() not in taken_names:
1✔
347
            return base_name
1✔
348
        index = 1
1✔
349
        while True:
1✔
350
            candidate = f"{base_name}_{index}"
1✔
351
            if candidate.lower() not in taken_names:
1✔
352
                return candidate
1✔
353
            index += 1
×
354

355
    def _unique_helper_import_alias(base_name: str) -> str:
1✔
356
        local_names = (
1✔
357
            _helper_import_local_names() | set(static_constants) | reserved_property_default_names
358
        )
359
        return _unique_generated_name(base_name, local_names)
1✔
360

361
    def _register_runtime_dimension(dim_name: str) -> str:
1✔
362
        local_name = runtime_dimension_locals.get(dim_name)
1✔
363
        if local_name is None:
1✔
364
            local_base = f"dim_{dim_name}"
1✔
365
            local_taken = set(property_name_map) | {
1✔
366
                existing.lower() for existing in runtime_dimension_locals.values()
367
            }
368
            local_name = _unique_generated_name(local_base, local_taken)
1✔
369
            runtime_dimension_locals[dim_name] = local_name
1✔
370
            default_name = _unique_helper_import_alias(f"{dim_name}_default")
1✔
371
            _add_helper_import(f"{default_name}=>{dim_name}")
1✔
372
            runtime_dimensions.append(
1✔
373
                {
374
                    "name": dim_name,
375
                    "default_name": default_name,
376
                    "local_name": local_name,
377
                }
378
            )
379
        return local_name
1✔
380

381
    current_property: str | None = None
1✔
382
    try:
1✔
383
        for index, (display_name, attr_name, prop) in enumerate(property_items):
1✔
384
            current_property = display_name
1✔
385
            name = attr_name
1✔
386
            _reject_runtime_dimension_lengths(prop, runtime_dimension_values)
1✔
387
            type_info = _field_type_info(prop, static_constants)
1✔
388
            for const_name in _collect_dimension_constants(type_info.dimensions, shape_constants):
1✔
389
                const_key = const_name.lower()
1✔
390
                if const_key in runtime_dimension_values:
1✔
391
                    _register_runtime_dimension(const_key)
1✔
392
                else:
393
                    _add_helper_import(const_name)
1✔
394
            if type_info.length_expr and not _is_int_literal(type_info.length_expr):
1✔
395
                _add_helper_import(type_info.length_expr)
1✔
396
            if type_info.kind:
1✔
397
                kind_ids.append(type_info.kind)
1✔
398

399
            runtime_shape = list(type_info.dimensions)
1✔
400
            dynamic_shape = False
1✔
401
            if type_info.category == "array":
1✔
402
                for dim_index, dim in enumerate(runtime_shape):
1✔
403
                    if dim == ":" or _is_int_literal(dim):
1✔
404
                        continue
1✔
405
                    if not FORTRAN_IDENTIFIER.match(dim):
1✔
406
                        raise ValueError(
×
407
                            "array property 'x-fortran-shape' entries must be ints or identifiers"
408
                        )
409
                    dim_key = dim.lower()
1✔
410
                    if dim_key in runtime_dimension_values:
1✔
411
                        local_dim_name = _register_runtime_dimension(dim_key)
1✔
412
                        runtime_shape[dim_index] = f"this%{local_dim_name}"
1✔
413
                        dynamic_shape = True
1✔
414
                    elif dim_key not in static_constants:
1✔
415
                        raise ValueError(f"dimension constant '{dim}' is not defined in config")
×
416

417
            runtime_length_expr = type_info.length_expr
1✔
418
            if (
1✔
419
                type_info.length_expr
420
                and not _is_int_literal(type_info.length_expr)
421
                and type_info.length_expr.lower() in runtime_dimension_values
422
            ):
423
                raise ValueError(
×
424
                    f"dimension '{type_info.length_expr}' cannot be used as x-fortran-len"
425
                )
426
            type_spec_with_defaults = type_info.type_spec
1✔
427

428
            array_default_info: tuple[Any, bool] | None = None
1✔
429
            if type_info.category == "array":
1✔
430
                array_default_info = _array_default_value(prop)
1✔
431

432
            flex_dim = _parse_flex_dim(prop, type_info)
1✔
433
            if flex_dim > 0:
1✔
434
                if type_info.element_category == "boolean":
1✔
435
                    raise ValueError("flex arrays cannot use boolean elements")
1✔
436
                if array_default_info is not None or any(
1✔
437
                    key in prop
438
                    for key in (
439
                        "x-fortran-default-order",
440
                        "x-fortran-default-repeat",
441
                        "x-fortran-default-pad",
442
                    )
443
                ):
UNCOV
444
                    raise ValueError("flex arrays cannot define defaults")
×
445

446
            is_runtime_sized = (
1✔
447
                type_info.category == "array" and dynamic_shape
448
            )
449

450
            if is_runtime_sized:
1✔
451
                declaration = _render_runtime_declaration(
1✔
452
                    type_info,
453
                    name,
454
                )
455
                local_decl = _render_runtime_local_declaration(
1✔
456
                    type_info,
457
                    name,
458
                    runtime_dimensions=runtime_shape,
459
                )
460
                runtime_allocations.extend(
1✔
461
                    _render_runtime_allocations(
462
                        type_info,
463
                        name,
464
                        runtime_dimensions=runtime_shape,
465
                        runtime_length_expr=runtime_length_expr,
466
                        target_prefix="this%",
467
                    )
468
                )
469
                runtime_deallocations.extend(
1✔
470
                    _render_runtime_deallocations(name, target_prefix="this%")
471
                )
472
                runtime_local_allocations.extend(
1✔
473
                    _render_runtime_local_allocations(
474
                        type_info,
475
                        name,
476
                        runtime_dimensions=runtime_shape,
477
                        runtime_length_expr=runtime_length_expr,
478
                    )
479
                )
480
            else:
481
                declaration = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
482
                local_decl = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
483

484
            title_raw = prop.get("title")
1✔
485
            if title_raw is None:
1✔
486
                title = display_name
1✔
487
            elif not isinstance(title_raw, str):
1✔
488
                raise ValueError(f"property '{display_name}' title must be a string")
×
489
            else:
490
                title = title_raw.strip() or name
1✔
491
            description = prop.get("description")
1✔
492
            declaration_with_doc = f"{declaration} !< {title}"
1✔
493

494
            dynamic_array = type_info.category == "array" and is_runtime_sized
1✔
495

496
            is_required = name in required_set
1✔
497
            if type_info.category == "array":
1✔
498
                has_default = array_default_info is not None
1✔
499
            else:
500
                has_default = "default" in prop
1✔
501

502
            default_from_items = False
1✔
503
            default_values: list[Any] | None = None
1✔
504
            parsed_dims: list[int] | None = None
1✔
505
            array_default_spec: ArrayDefaultSpec | None = None
1✔
506

507
            if type_info.category == "array" and has_default:
1✔
508
                if array_default_info is None:
1✔
509
                    raise ValueError(f"missing array default for '{display_name}'")
×
510
                default_raw, default_from_items = array_default_info
1✔
511
                if default_from_items:
1✔
512
                    if isinstance(default_raw, list):
1✔
513
                        raise ValueError("array items default must be a scalar")
×
514
                    default_values = [default_raw]
1✔
515
                else:
516
                    if not isinstance(default_raw, list):
1✔
517
                        raise ValueError("array default must be a list")
×
518
                    default_values = default_raw
1✔
519
                    parsed_dims = _parse_default_dimensions(type_info.dimensions, shape_constants)
1✔
520
                    array_default_spec = _prepare_array_default(default_values, parsed_dims, prop)
1✔
521

522
            needs_sentinel = (not is_required) and (not has_default)
1✔
523
            requires_sentinel = is_required or needs_sentinel
1✔
524

525
            arg_dimensions = type_info.dimensions
1✔
526
            if type_info.category == "array":
1✔
527
                arg_dimensions = [":" for _ in type_info.dimensions]
1✔
528
            argument_decl = _render_argument_declaration(
1✔
529
                name=name,
530
                type_info=type_info,
531
                is_required=is_required,
532
                dimensions=arg_dimensions,
533
                doc=title,
534
            )
535
            local_init_assignments.append(f"{name} = this%{name}")
1✔
536

537
            sentinel_assignment: str | None = None
1✔
538
            sentinel_condition: str | None = None
1✔
539
            set_sentinel_condition: str | None = None
1✔
540
            if requires_sentinel:
1✔
541
                if is_required and type_info.category == "boolean":
1✔
542
                    raise ValueError(
×
543
                        f"required {type_info.category} '{display_name}' is not supported"
544
                    )
545
                if is_required and type_info.category == "array":
1✔
546
                    if type_info.element_category == "boolean":
1✔
547
                        raise ValueError("required boolean arrays are not supported")
×
548
                if needs_sentinel and type_info.category == "boolean":
1✔
549
                    raise ValueError(f"optional boolean '{display_name}' must define a default")
×
550
                if needs_sentinel and type_info.category == "array":
1✔
551
                    if type_info.element_category == "boolean":
1✔
552
                        raise ValueError("optional boolean arrays must define a default")
×
553
                value_expr, condition_expr, uses_ieee = _sentinel_expressions(
1✔
554
                    type_info,
555
                    var_ref=f"this%{name}",
556
                )
557
                sentinel_assignment = _render_sentinel_assignment(
1✔
558
                    type_info,
559
                    target_ref=f"this%{name}",
560
                    value_expr=value_expr,
561
                    comment=_sentinel_comment(type_info, required=is_required),
562
                )
563
                sentinel_condition = condition_expr
1✔
564
                sentinel_assignments.append(sentinel_assignment)
1✔
565
                if uses_ieee:
1✔
566
                    requires_ieee = True
1✔
567
                if not has_default and type_info.category != "boolean":
1✔
568
                    set_value_expr, set_condition_expr, set_uses_ieee = _sentinel_expressions(
1✔
569
                        type_info,
570
                        var_ref=f"this%{name}",
571
                    )
572
                    if set_uses_ieee:
1✔
573
                        requires_ieee = True
1✔
574
                    set_sentinel_condition = set_condition_expr
1✔
575
                    if needs_sentinel:
1✔
576
                        sent_com = _sentinel_comment(type_info, required=False)
1✔
577
                        set_optional_defaults.append(
1✔
578
                            _render_sentinel_assignment(
579
                                type_info,
580
                                target_ref=f"this%{name}",
581
                                value_expr=set_value_expr,
582
                                comment=sent_com,
583
                            )
584
                        )
585

586
            if is_required and type_info.category != "array":
1✔
587
                required_scalar_names.add(name)
1✔
588

589
            if is_required and type_info.category == "array" and flex_dim == 0:
1✔
590
                element_category = type_info.element_category
1✔
591
                if element_category is None:
1✔
592
                    raise ValueError("array field missing element category")
×
593
                all_missing, any_missing, uses_ieee = _array_missing_conditions(
1✔
594
                    element_category,
595
                    var_ref=_array_section_ref(f"this%{name}", len(type_info.dimensions)),
596
                    len_ref=f"this%{name}",
597
                )
598
                required_array_by_name[name] = {
1✔
599
                    "name": display_name,
600
                    "attr_name": name,
601
                    "runtime_array": dynamic_array,
602
                    "all_missing_condition": all_missing,
603
                    "any_missing_condition": any_missing,
604
                }
605
                uses_partly_set = True
1✔
606
                if uses_ieee:
1✔
607
                    requires_ieee = True
1✔
608

609
            partial_bounds: list[dict[str, Any]] | None = None
1✔
610
            if type_info.category == "array":
1✔
611
                rank = len(type_info.dimensions)
1✔
612
                all_bounds = []
1✔
613
                for array_dim_index in range(1, rank + 1):
1✔
614
                    lb_var, ub_var = _flex_bound_vars(array_dim_index)
1✔
615
                    all_bounds.append(
1✔
616
                        {"dim": array_dim_index, "lb_var": lb_var, "ub_var": ub_var}
617
                    )
618
                    flex_bound_vars.add(lb_var)
1✔
619
                    flex_bound_vars.add(ub_var)
1✔
620
                partial_bounds = all_bounds
1✔
621
            if flex_dim > 0:
1✔
622
                rank = len(type_info.dimensions)
1✔
623
                element_category = type_info.element_category
1✔
624
                if element_category is None:
1✔
625
                    raise ValueError("array field missing element category")
×
626
                flex_dims: list[int] = list(range(rank - flex_dim + 1, rank + 1))
1✔
627
                slice_missing_conditions: list[str] = []
1✔
628
                slice_uses_ieee = False
1✔
629
                flex_dim_bounds: list[dict[str, Any]] = []
1✔
630
                lb_vars: dict[int, str] = {}
1✔
631
                ub_vars: dict[int, str] = {}
1✔
632
                for flex_dim_index in flex_dims:
1✔
633
                    lb_var, ub_var = _flex_bound_vars(flex_dim_index)
1✔
634
                    lb_vars[flex_dim_index] = lb_var
1✔
635
                    ub_vars[flex_dim_index] = ub_var
1✔
636
                    flex_dim_bounds.append(
1✔
637
                        {"dim": flex_dim_index, "lb_var": lb_var, "ub_var": ub_var}
638
                    )
639
                    flex_bound_vars.add(lb_var)
1✔
640
                    flex_bound_vars.add(ub_var)
1✔
641
                    slice_ref = _slice_ref(name, rank, flex_dim_index, "idx")
1✔
642
                    slice_missing_expr, uses_ieee = _element_missing_expression(
1✔
643
                        element_category,
644
                        var_ref=slice_ref,
645
                        len_ref=f"this%{name}",
646
                    )
647
                    slice_missing_conditions.append(
1✔
648
                        f"all({slice_missing_expr})" if rank > 1 else slice_missing_expr
649
                    )
650
                    slice_uses_ieee = slice_uses_ieee or uses_ieee
1✔
651
                prefix_ref = _slice_ref_bounds(name, rank, flex_dims, lb_vars, ub_vars)
1✔
652
                prefix_missing_expr, uses_ieee_prefix = _element_missing_expression(
1✔
653
                    element_category,
654
                    var_ref=prefix_ref,
655
                    len_ref=f"this%{name}",
656
                )
657
                prefix_any_missing_condition = f"any({prefix_missing_expr})"
1✔
658
                flex_arrays.append(
1✔
659
                    {
660
                        "name": name,
661
                        "display_name": display_name,
662
                        "rank": rank,
663
                        "flex_dims": flex_dims,
664
                        "required": is_required,
665
                        "runtime_array": dynamic_array,
666
                        "bounds": flex_dim_bounds,
667
                        "slice_missing_conditions": slice_missing_conditions,
668
                        "prefix_any_missing_condition": prefix_any_missing_condition,
669
                    }
670
                )
671
                uses_partly_set = True
1✔
672
                if slice_uses_ieee or uses_ieee_prefix:
1✔
673
                    requires_ieee = True
×
674

675
            default_assignment: str | None = None
1✔
676
            set_default_assignment: str | None = None
1✔
677
            if has_default and is_required:
1✔
678
                raise ValueError(f"required property '{display_name}' cannot define a default")
×
679
            if has_default:
1✔
680
                default_const_name = f"{name}_default"
1✔
681
                if type_info.category == "array":
1✔
682
                    if default_values is None:
1✔
683
                        raise ValueError(f"missing array default for '{display_name}'")
×
684
                    default_is_scalar = default_from_items
1✔
685

686
                    repeat = False
1✔
687
                    pad_raw = None
1✔
688
                    pad_const_name: str | None = None
1✔
689
                    pad_is_scalar = False
1✔
690

691
                    if not default_from_items:
1✔
692
                        repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
693
                        if not isinstance(repeat_raw, bool):
1✔
694
                            raise ValueError("array default repeat must be a boolean")
×
695
                        repeat = bool(repeat_raw)
1✔
696
                        pad_raw = prop.get("x-fortran-default-pad")
1✔
697
                        if pad_raw is not None:
1✔
698
                            pad_const_name = f"{name}_pad"
1✔
699
                            pad_is_scalar = not isinstance(pad_raw, list)
1✔
700
                            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
1✔
701
                            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
1✔
702
                            pad_elements = [
1✔
703
                                _format_scalar_default(
704
                                    element, type_info.kind, type_info.element_category
705
                                )
706
                                for element in pad_values
707
                            ]
708
                            if pad_is_scalar:
1✔
709
                                pad_literal = _format_scalar_default(
1✔
710
                                    pad_raw, type_info.kind, type_info.element_category
711
                                )
712
                                default_parameters.append(
1✔
713
                                    f"{type_spec_with_defaults}, parameter, public :: "
714
                                    f"{pad_const_name} = {pad_literal}"
715
                                )
716
                            else:
717
                                default_parameters.append(
×
718
                                    f"{type_spec_with_defaults}, parameter, public :: "
719
                                    f"{pad_const_name}({len(pad_elements)}) = "
720
                                    f"[{', '.join(pad_elements)}]"
721
                                )
722

723
                        if parsed_dims is None:
1✔
724
                            parsed_dims = _parse_default_dimensions(
×
725
                                type_info.dimensions, shape_constants
726
                            )
727
                        if array_default_spec is None:
1✔
728
                            array_default_spec = _prepare_array_default(
×
729
                                default_values, parsed_dims, prop
730
                            )
731

732
                    if default_is_scalar:
1✔
733
                        default_literal = _format_scalar_default(
1✔
734
                            default_values[0], type_info.kind, type_info.element_category
735
                        )
736
                        default_parameters.append(
1✔
737
                            f"{type_spec_with_defaults}, parameter, public :: "
738
                            f"{default_const_name} = {default_literal}"
739
                        )
740
                    else:
741
                        if array_default_spec is None:
1✔
742
                            raise ValueError(
×
743
                                f"missing array default specification for '{display_name}'"
744
                            )
745
                        default_elements = [
1✔
746
                            _format_scalar_default(
747
                                element, type_info.kind, type_info.element_category
748
                            )
749
                            for element in array_default_spec.source_values
750
                        ]
751
                        default_parameters.append(
1✔
752
                            f"{type_spec_with_defaults}, parameter, public :: "
753
                            f"{default_const_name}({len(default_elements)}) = "
754
                            f"[{', '.join(default_elements)}]"
755
                        )
756

757
                    if default_from_items:
1✔
758
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
759
                    elif (
1✔
760
                        len(type_info.dimensions) == 1
761
                        and array_default_spec is not None
762
                        and array_default_spec.order_values is None
763
                        and array_default_spec.pad_values is None
764
                    ):
765
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
766
                    else:
767
                        source_expr = default_const_name
1✔
768
                        shape_expr = ", ".join(runtime_shape)
1✔
769
                        arguments = [source_expr, f"shape=[{shape_expr}]"]
1✔
770
                        if array_default_spec is not None:
1✔
771
                            if array_default_spec.order_values is not None:
1✔
772
                                order_literal = ", ".join(
1✔
773
                                    str(index) for index in array_default_spec.order_values
774
                                )
775
                                arguments.append(f"order=[{order_literal}]")
1✔
776
                            if array_default_spec.pad_values is not None:
1✔
777
                                if repeat:
1✔
778
                                    pad_expr = default_const_name
1✔
779
                                else:
780
                                    if pad_const_name is None:
1✔
781
                                        raise ValueError(
×
782
                                            f"missing pad values for array default '{display_name}'"
783
                                        )
784
                                    pad_expr = (
1✔
785
                                        pad_const_name
786
                                        if not pad_is_scalar
787
                                        else f"[{pad_const_name}]"
788
                                    )
789
                                arguments.append(f"pad={pad_expr}")
1✔
790
                        default_assignment = _format_reshape_assignment(name, arguments)
1✔
791
                    set_default_assignment = default_assignment
1✔
792
                else:
793
                    default_literal = _format_default(prop["default"], type_info, prop, constants)
1✔
794
                    default_parameters.append(
1✔
795
                        f"{type_spec_with_defaults}, parameter, public :: "
796
                        f"{default_const_name} = {default_literal}"
797
                    )
798
                    if type_info.category == "boolean":
1✔
799
                        default_assignment = (
1✔
800
                            f"this%{name} = {default_const_name} "
801
                            "! bool values always need a default"
802
                        )
803
                    else:
804
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
805
                    set_default_assignment = f"this%{name} = {default_const_name}"
1✔
806

807
                if default_assignment is None or set_default_assignment is None:
1✔
808
                    raise ValueError(f"missing default assignment for '{display_name}'")
×
809
                default_assignments.append(default_assignment)
1✔
810
                set_optional_defaults.append(set_default_assignment)
1✔
811

812
            if type_info.category == "array" and any(
1✔
813
                (dim != ":") and (not _is_int_literal(dim)) for dim in type_info.dimensions
814
            ):
815
                required_default_elements: int | None = None
1✔
816
                if has_default and default_values is not None:
1✔
817
                    if default_from_items:
1✔
818
                        required_default_elements = 1
1✔
819
                    elif array_default_spec is not None:
1✔
820
                        required_default_elements = len(array_default_spec.source_values)
1✔
821
                    else:
822
                        required_default_elements = len(default_values)
×
823
                if required_default_elements is not None and required_default_elements > 1:
1✔
824
                    runtime_default_extent_requirements.append(
1✔
825
                        {
826
                            "field": display_name,
827
                            "dimensions": list(type_info.dimensions),
828
                            "required_elements": required_default_elements,
829
                        }
830
                    )
831

832
            enum_values = _enum_values(prop, type_info, constants)
1✔
833
            if enum_values is not None:
1✔
834
                enum_category = _enum_category(type_info)
1✔
835
                enum_const_name = f"{name}_enum_values"
1✔
836
                enum_literals = [
1✔
837
                    _format_scalar_default(value, type_info.kind, enum_category)
838
                    for value in enum_values
839
                ]
840
                if enum_category == "string":
1✔
841
                    enum_array_literal = (
1✔
842
                        f"[{type_spec_with_defaults} :: {', '.join(enum_literals)}]"
843
                    )
844
                else:
845
                    enum_array_literal = f"[{', '.join(enum_literals)}]"
1✔
846
                if enum_category == "string":
1✔
847
                    enum_parameters.append(
1✔
848
                        f"{type_spec_with_defaults}, parameter, public :: &\n"
849
                        f"    {enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
850
                    )
851
                else:
852
                    enum_parameters.append(
1✔
853
                        f"{type_spec_with_defaults}, parameter, public :: "
854
                        f"{enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
855
                    )
856
                enum_type_info = (
1✔
857
                    _element_type_info(type_info) if type_info.category == "array" else type_info
858
                )
859
                _, missing_condition, _ = _sentinel_expressions(
1✔
860
                    enum_type_info,
861
                    var_ref="val",
862
                    len_ref="val",
863
                )
864
                enum_functions.append(
1✔
865
                    {
866
                        "name": name,
867
                        "func_name": f"{name}_in_enum",
868
                        "arg_type_spec": _enum_arg_type_spec(type_info),
869
                        "enum_values_name": enum_const_name,
870
                        "use_trim": enum_category == "string",
871
                        "missing_condition": missing_condition,
872
                    }
873
                )
874
                if type_info.category == "array":
1✔
875
                    enum_checks.append(
1✔
876
                        {
877
                            "name": name,
878
                            "display_name": display_name,
879
                            "func_name": f"{name}_in_enum",
880
                            "is_array": True,
881
                            "runtime_array": dynamic_array,
882
                            "array_ref": f"this%{name}",
883
                        }
884
                    )
885
                else:
886
                    enum_checks.append(
1✔
887
                        {
888
                            "name": name,
889
                            "display_name": display_name,
890
                            "func_name": f"{name}_in_enum",
891
                            "is_array": False,
892
                            "element_ref": f"this%{name}",
893
                        }
894
                    )
895

896
            bounds_spec = _bounds_spec(prop, type_info)
1✔
897
            if bounds_spec is not None:
1✔
898
                bounds_category = bounds_spec["category"]
1✔
899
                bounds_type_info = (
1✔
900
                    _element_type_info(type_info) if type_info.category == "array" else type_info
901
                )
902
                min_value = bounds_spec["min_value"]
1✔
903
                max_value = bounds_spec["max_value"]
1✔
904
                min_exclusive = bounds_spec["min_exclusive"]
1✔
905
                max_exclusive = bounds_spec["max_exclusive"]
1✔
906
                min_name = None
1✔
907
                max_name = None
1✔
908
                if min_value is not None:
1✔
909
                    min_name = f"{name}_min_excl" if min_exclusive else f"{name}_min"
1✔
910
                    min_literal = _format_scalar_default(
1✔
911
                        min_value, bounds_type_info.kind, bounds_category
912
                    )
913
                    bounds_parameters.append(
1✔
914
                        f"{bounds_type_info.type_spec}, parameter, public :: "
915
                        f"{min_name} = {min_literal}"
916
                    )
917
                if max_value is not None:
1✔
918
                    max_name = f"{name}_max_excl" if max_exclusive else f"{name}_max"
1✔
919
                    max_literal = _format_scalar_default(
1✔
920
                        max_value, bounds_type_info.kind, bounds_category
921
                    )
922
                    bounds_parameters.append(
1✔
923
                        f"{bounds_type_info.type_spec}, parameter, public :: "
924
                        f"{max_name} = {max_literal}"
925
                    )
926
                _, missing_condition, uses_ieee = _sentinel_expressions(
1✔
927
                    bounds_type_info,
928
                    var_ref="val",
929
                    len_ref="val",
930
                )
931
                if uses_ieee:
1✔
932
                    requires_ieee = True
1✔
933
                bounds_functions.append(
1✔
934
                    {
935
                        "name": name,
936
                        "func_name": f"{name}_in_bounds",
937
                        "arg_type_spec": bounds_type_info.arg_type_spec,
938
                        "has_min": min_value is not None,
939
                        "has_max": max_value is not None,
940
                        "min_name": min_name,
941
                        "max_name": max_name,
942
                        "min_exclusive": min_exclusive,
943
                        "max_exclusive": max_exclusive,
944
                        "missing_condition": missing_condition,
945
                    }
946
                )
947
                if type_info.category == "array":
1✔
948
                    bounds_checks.append(
1✔
949
                        {
950
                            "name": name,
951
                            "display_name": display_name,
952
                            "func_name": f"{name}_in_bounds",
953
                            "is_array": True,
954
                            "runtime_array": dynamic_array,
955
                            "array_ref": f"this%{name}",
956
                        }
957
                    )
958
                else:
959
                    bounds_checks.append(
1✔
960
                        {
961
                            "name": name,
962
                            "display_name": display_name,
963
                            "func_name": f"{name}_in_bounds",
964
                            "is_array": False,
965
                            "element_ref": f"this%{name}",
966
                        }
967
                    )
968

969
            is_array = type_info.category == "array"
1✔
970
            if is_required:
1✔
971
                if is_array:
1✔
972
                    # Match namelist-buffer semantics: set assigns the provided
973
                    # leading subsection and leaves completeness checks to is_valid.
974
                    set_required_assignments.append(
1✔
975
                        _render_partial_set_block(
976
                            name,
977
                            len(type_info.dimensions),
978
                            partial_bounds or [],
979
                        )
980
                    )
981
                else:
982
                    set_required_assignments.append(f"this%{name} = {name}")
1✔
983

984
            if not is_required:
1✔
985
                if is_array:
1✔
986
                    # Match namelist-buffer semantics: set assigns the provided
987
                    # leading subsection and leaves completeness checks to is_valid.
988
                    block = _render_partial_set_block(
1✔
989
                        name,
990
                        len(type_info.dimensions),
991
                        partial_bounds or [],
992
                    )
993
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
1✔
994
                    set_present_assignment = (
1✔
995
                        f"if (present({name})) then\n{indented_block}\nend if"
996
                    )
997
                else:
998
                    set_present_assignment = f"if (present({name})) this%{name} = {name}"
1✔
999
            else:
1000
                set_present_assignment = None
1✔
1001

1002
            array_rank = len(type_info.dimensions) if is_array else 0
1✔
1003
            element_condition: str | None = None
1✔
1004

1005
            if is_array and not has_default:
1✔
1006
                element_type = _element_type_info(type_info)
1✔
1007
                index_args = ", ".join(f"idx({idx})" for idx in range(1, array_rank + 1))
1✔
1008
                element_ref = f"this%{name}({index_args})"
1✔
1009
                _, element_condition, element_uses_ieee = _sentinel_expressions(
1✔
1010
                    element_type,
1011
                    var_ref=element_ref,
1012
                    len_ref=f"this%{name}",
1013
                )
1014
                if element_uses_ieee:
1✔
1015
                    requires_ieee = True
1✔
1016

1017
            if has_default or type_info.category == "boolean":
1✔
1018
                presence_cases.append(
1✔
1019
                    {
1020
                        "name": name,
1021
                        "display_name": display_name,
1022
                        "always_true": True,
1023
                        "sentinel_condition": None,
1024
                        "is_array": is_array,
1025
                        "runtime_array": dynamic_array,
1026
                        "rank": array_rank,
1027
                        "element_condition": element_condition,
1028
                    }
1029
                )
1030
            else:
1031
                if set_sentinel_condition is None:
1✔
1032
                    raise ValueError(f"missing sentinel condition for '{display_name}'")
×
1033
                presence_cases.append(
1✔
1034
                    {
1035
                        "name": name,
1036
                        "display_name": display_name,
1037
                        "always_true": False,
1038
                        "sentinel_condition": set_sentinel_condition,
1039
                        "is_array": is_array,
1040
                        "runtime_array": dynamic_array,
1041
                        "rank": array_rank,
1042
                        "element_condition": element_condition,
1043
                    }
1044
                )
1045

1046
            fields.append(
1✔
1047
                FieldSpec(
1048
                    order=index,
1049
                    name=name,
1050
                    title=title,
1051
                    description=description,
1052
                    declaration=declaration_with_doc,
1053
                    local_declaration=local_decl,
1054
                    required=is_required,
1055
                    sentinel_assignment=sentinel_assignment,
1056
                    sentinel_check=(
1057
                        f"if ({sentinel_condition}) error stop "
1058
                        f"\"{module_name}%from_file: '{name}' is required\""
1059
                        if sentinel_condition and is_required
1060
                        else None
1061
                    ),
1062
                    default_assignment=default_assignment,
1063
                    set_default_assignment=set_default_assignment,
1064
                    set_present_assignment=set_present_assignment,
1065
                    argument_declaration=argument_decl,
1066
                    type_category=type_info.category,
1067
                    runtime_sized_array=dynamic_array,
1068
                    rank=len(type_info.dimensions),
1069
                )
1070
            )
1071

1072
    except ValueError as exc:
1✔
1073
        if current_property is None:
1✔
1074
            raise
×
1075
        msg = str(exc)
1✔
1076
        if f"property '{current_property}'" in msg:
1✔
1077
            raise
×
1078
        raise ValueError(f"property '{current_property}': {msg}") from exc
1✔
1079
    if uses_partly_set and "NML_ERR_PARTLY_SET" not in helper_imports:
1✔
1080
        helper_imports.append("NML_ERR_PARTLY_SET")
1✔
1081
    required_flex_names = {entry["name"] for entry in flex_arrays if entry["required"]}
1✔
1082
    required_scalar_validations: list[str] = []
1✔
1083
    for name in required_fields:
1✔
1084
        if name in required_scalar_names:
1✔
1085
            required_scalar_validations.append(property_name_map[name])
1✔
1086
        elif name not in required_array_by_name and name not in required_flex_names:
1✔
1087
            required_scalar_validations.append(property_name_map[name])
×
1088
    required_array_validations = [
1✔
1089
        required_array_by_name[name]
1090
        for name in required_fields
1091
        if name in required_array_by_name
1092
    ]
1093
    namelist_vars = [field.name for field in fields]
1✔
1094
    required_fields_specs = [field for field in fields if field.required]
1✔
1095
    optional_fields_specs = [field for field in fields if not field.required]
1✔
1096

1097
    resolved_kind_module = kind_module or "iso_fortran_env"
1✔
1098
    if not isinstance(resolved_kind_module, str) or not resolved_kind_module:
1✔
1099
        raise ValueError("kind module must be a non-empty string")
×
1100

1101
    set_dims_arguments: list[dict[str, Any]] = []
1✔
1102
    candidate_names_in_use: set[str] = set()
1✔
1103
    for entry in runtime_dimensions:
1✔
1104
        candidate_base = f"candidate_{entry['name']}"
1✔
1105
        candidate_name = _unique_generated_name(candidate_base, candidate_names_in_use)
1✔
1106
        candidate_names_in_use.add(candidate_name.lower())
1✔
1107
        set_dims_arguments.append(
1✔
1108
            {
1109
                "name": entry["name"],
1110
                "default_name": entry["default_name"],
1111
                "local_name": entry["local_name"],
1112
                "arg_name": entry["name"],
1113
                "candidate_name": candidate_name,
1114
                "min_required": 1,
1115
            }
1116
        )
1117

1118
    candidate_name_map: dict[str, str] = {
1✔
1119
        str(entry["name"]): str(entry["candidate_name"]) for entry in set_dims_arguments
1120
    }
1121
    set_dims_extent_checks: list[dict[str, str]] = []
1✔
1122
    seen_extent_checks: set[tuple[str, str]] = set()
1✔
1123
    for requirement in runtime_default_extent_requirements:
1✔
1124
        factors: list[str] = []
1✔
1125
        requirement_dimensions = cast("list[str]", requirement["dimensions"])
1✔
1126
        for extent_dim in requirement_dimensions:
1✔
1127
            if extent_dim == ":":
1✔
1128
                continue
×
1129
            if _is_int_literal(extent_dim):
1✔
1130
                factors.append(extent_dim)
1✔
1131
            else:
1132
                factors.append(candidate_name_map.get(extent_dim.lower(), extent_dim))
1✔
1133
        if not factors:
1✔
1134
            continue
×
1135
        product_expr = " * ".join(factors)
1✔
1136
        if len(factors) > 1:
1✔
1137
            product_expr = f"({product_expr})"
1✔
1138
        required_elements = cast("int", requirement["required_elements"])
1✔
1139
        condition = f"{product_expr} < {required_elements}"
1✔
1140
        field_name = cast("str", requirement["field"])
1✔
1141
        extent_message = (
1✔
1142
            f"shape constants for '{field_name}' must allow at least "
1143
            f"{required_elements} default values"
1144
        )
1145
        extent_key = (condition, extent_message)
1✔
1146
        if extent_key in seen_extent_checks:
1✔
1147
            continue
×
1148
        seen_extent_checks.add(extent_key)
1✔
1149
        set_dims_extent_checks.append({"condition": condition, "message": extent_message})
1✔
1150

1151
    context = {
1✔
1152
        "module_name": module_name,
1153
        "type_name": type_name,
1154
        "type_prefix": module_name,
1155
        "doc_class": doc_class,
1156
        "brief_text": brief_text,
1157
        "details_text": details_text,
1158
        "module_doc": module_doc,
1159
        "namelist_name": namelist_name,
1160
        "fields": fields,
1161
        "runtime_dimensions": runtime_dimensions,
1162
        "runtime_allocations": runtime_allocations,
1163
        "runtime_deallocations": runtime_deallocations,
1164
        "runtime_local_allocations": runtime_local_allocations,
1165
        "namelist_vars": namelist_vars,
1166
        "sentinel_assignments": sentinel_assignments,
1167
        "default_assignments": default_assignments,
1168
        "default_parameters": default_parameters,
1169
        "enum_parameters": enum_parameters,
1170
        "bounds_parameters": bounds_parameters,
1171
        "local_init_assignments": local_init_assignments,
1172
        "required_scalar_validations": required_scalar_validations,
1173
        "required_array_validations": required_array_validations,
1174
        "flex_arrays": flex_arrays,
1175
        "assignments": [f"this%{field.name} = {field.name}" for field in fields],
1176
        "argument_list": [field.name for field in required_fields_specs + optional_fields_specs],
1177
        "required_argument_declarations": [
1178
            field.argument_declaration for field in required_fields_specs
1179
        ],
1180
        "optional_argument_declarations": [
1181
            field.argument_declaration for field in optional_fields_specs
1182
        ],
1183
        "set_dims_arguments": set_dims_arguments,
1184
        "set_dims_extent_checks": set_dims_extent_checks,
1185
        "set_required_assignments": set_required_assignments,
1186
        "set_optional_defaults": set_optional_defaults,
1187
        "set_optional_present": [
1188
            field.set_present_assignment
1189
            for field in optional_fields_specs
1190
            if field.set_present_assignment
1191
        ],
1192
        "enum_functions": enum_functions,
1193
        "enum_checks": enum_checks,
1194
        "bounds_functions": bounds_functions,
1195
        "bounds_checks": bounds_checks,
1196
        "kind_module": resolved_kind_module,
1197
        "kind_imports": _resolve_kind_imports(
1198
            kind_ids,
1199
            kind_map=kind_map,
1200
            kind_allowlist=kind_allowlist,
1201
        ),
1202
        "use_ieee": requires_ieee,
1203
        "helper_module": helper_module,
1204
        "helper_imports": helper_imports,
1205
        "presence_cases": presence_cases,
1206
        "flex_bound_vars": _sort_bound_vars(flex_bound_vars),
1207
        "f2py_handle_helpers": f2py_handle_helpers,
1208
    }
1209

1210
    return context
1✔
1211

1212

1213
def _ordered_unique(values: Iterable[Any]) -> list[Any]:
1✔
1214
    seen: set[Any] = set()
1✔
1215
    ordered: list[Any] = []
1✔
1216
    for value in values:
1✔
1217
        if value not in seen:
1✔
1218
            seen.add(value)
1✔
1219
            ordered.append(value)
1✔
1220
    return ordered
1✔
1221

1222

1223
def _reject_runtime_dimension_lengths(
1✔
1224
    prop: dict[str, Any],
1225
    dimensions: dict[str, int],
1226
) -> None:
1227
    if not dimensions:
1✔
1228
        return
1✔
1229
    if prop.get("type") == "string":
1✔
1230
        length = prop.get("x-fortran-len")
1✔
1231
        if isinstance(length, str) and length.strip().lower() in dimensions:
1✔
1232
            raise ValueError(f"dimension '{length.strip()}' cannot be used as x-fortran-len")
1✔
1233
    if prop.get("type") == "array":
1✔
1234
        items = prop.get("items")
1✔
1235
        if isinstance(items, dict):
1✔
1236
            _reject_runtime_dimension_lengths(items, dimensions)
1✔
1237

1238

1239
def _resolve_kind_imports(
1✔
1240
    kind_ids: list[str],
1241
    *,
1242
    kind_map: dict[str, str] | None,
1243
    kind_allowlist: Iterable[str] | None,
1244
) -> list[str]:
1245
    allowlist = set(kind_allowlist) if kind_allowlist is not None else None
1✔
1246
    imports: list[str] = []
1✔
1247
    target_aliases: dict[str, str] = {}
1✔
1248
    for kind_id in _ordered_unique(kind_ids):
1✔
1249
        target = kind_id
1✔
1250
        if kind_map is not None and kind_id in kind_map:
1✔
1251
            mapped = kind_map[kind_id]
1✔
1252
            if not isinstance(mapped, str):
1✔
1253
                raise ValueError(f"kind map target for '{kind_id}' must be a string")
×
1254
            target = mapped
1✔
1255
        elif allowlist is not None and kind_id not in allowlist:
1✔
1256
            raise ValueError(f"kind '{kind_id}' not present in kind map or kind module list")
×
1257

1258
        if allowlist is not None and target not in allowlist:
1✔
1259
            raise ValueError(
×
1260
                f"kind map target '{target}' for '{kind_id}' not present in kind module list"
1261
            )
1262

1263
        if target != kind_id:
1✔
1264
            existing = target_aliases.get(target)
1✔
1265
            if existing is not None and existing != kind_id:
1✔
1266
                raise ValueError(
×
1267
                    f"kind map target '{target}' is shared by '{existing}' and '{kind_id}'"
1268
                )
1269
            target_aliases[target] = kind_id
1✔
1270
            imports.append(f"{kind_id}=>{target}")
1✔
1271
        else:
1272
            imports.append(kind_id)
1✔
1273
    return imports
1✔
1274

1275

1276
def _field_type_info(
1✔
1277
    prop: dict[str, Any],
1278
    constants: dict[str, int] | None,
1279
) -> FieldTypeInfo:
1280
    prop_type = prop.get("type")
1✔
1281
    if prop_type == "array":
1✔
1282
        dimensions: list[str] = []
1✔
1283
        current = prop
1✔
1284
        while current.get("type") == "array":
1✔
1285
            dimensions.extend(_extract_dimensions(current))
1✔
1286
            items = current.get("items")
1✔
1287
            if not isinstance(items, dict):
1✔
1288
                raise ValueError("array property must define 'items'")
×
1289
            if items.get("type") == "array":
1✔
1290
                raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
1291
            current = items
1✔
1292
        scalar = _scalar_type_info(current, constants)
1✔
1293
        return FieldTypeInfo(
1✔
1294
            type_spec=scalar.type_spec,
1295
            arg_type_spec=scalar.arg_type_spec,
1296
            dimensions=dimensions,
1297
            kind=scalar.kind,
1298
            category="array",
1299
            length_expr=scalar.length_expr,
1300
            element_category=scalar.category,
1301
        )
1302

1303
    scalar = _scalar_type_info(prop, constants)
1✔
1304
    return FieldTypeInfo(
1✔
1305
        type_spec=scalar.type_spec,
1306
        arg_type_spec=scalar.arg_type_spec,
1307
        dimensions=[],
1308
        kind=scalar.kind,
1309
        category=scalar.category,
1310
        length_expr=scalar.length_expr,
1311
        element_category=None,
1312
    )
1313

1314

1315
def _element_type_info(type_info: FieldTypeInfo) -> FieldTypeInfo:
1✔
1316
    if type_info.category != "array":
1✔
1317
        raise ValueError("element type info requires an array field")
×
1318
    element_category = type_info.element_category
1✔
1319
    if element_category is None:
1✔
1320
        raise ValueError("array field missing element category")
×
1321
    return FieldTypeInfo(
1✔
1322
        type_spec=type_info.type_spec,
1323
        arg_type_spec=type_info.type_spec,
1324
        dimensions=[],
1325
        kind=type_info.kind,
1326
        category=element_category,
1327
        length_expr=type_info.length_expr,
1328
        element_category=None,
1329
    )
1330

1331

1332
def _enum_category(type_info: FieldTypeInfo) -> str:
1✔
1333
    if type_info.category == "array":
1✔
1334
        if type_info.element_category is None:
1✔
1335
            raise ValueError("array field missing element category")
×
1336
        return type_info.element_category
1✔
1337
    return type_info.category
1✔
1338

1339

1340
def _enum_arg_type_spec(type_info: FieldTypeInfo) -> str:
1✔
1341
    category = _enum_category(type_info)
1✔
1342
    if category == "string":
1✔
1343
        return "character(len=*)"
1✔
1344
    return type_info.type_spec
1✔
1345

1346

1347
def _scalar_type_info(
1✔
1348
    prop: dict[str, Any],
1349
    constants: dict[str, int] | None,
1350
) -> ScalarTypeInfo:
1351
    prop_type = prop.get("type")
1✔
1352
    if prop_type == "string":
1✔
1353
        length = prop.get("x-fortran-len")
1✔
1354
        if isinstance(length, bool):
1✔
1355
            raise ValueError("string property must define integer 'x-fortran-len'")
×
1356
        if isinstance(length, int):
1✔
1357
            if length <= 0:
1✔
1358
                raise ValueError("string length must be positive")
×
1359
            length_expr = str(length)
1✔
1360
        elif isinstance(length, str):
1✔
1361
            length_expr = length.strip()
1✔
1362
            if not length_expr:
1✔
1363
                raise ValueError("string length must be a non-empty value")
×
1364
            _validate_length_token(length_expr)
1✔
1365
            if _is_int_literal(length_expr):
1✔
1366
                if int(length_expr) <= 0:
×
1367
                    raise ValueError("string length must be positive")
×
1368
            else:
1369
                length_key = length_expr.lower()
1✔
1370
                if constants is None or length_key not in constants:
1✔
1371
                    raise ValueError(
×
1372
                        f"string length constant '{length_expr}' is not defined in config"
1373
                    )
1374
                value = constants[length_key]
1✔
1375
                if isinstance(value, bool) or not isinstance(value, int):
1✔
1376
                    raise ValueError(f"string length constant '{length_expr}' must be an integer")
×
1377
                if value <= 0:
1✔
1378
                    raise ValueError(f"string length constant '{length_expr}' must be positive")
×
1379
        else:
1380
            raise ValueError("string property must define integer 'x-fortran-len'")
×
1381
        return ScalarTypeInfo(
1✔
1382
            type_spec=f"character(len={length_expr})",
1383
            arg_type_spec="character(len=*)",
1384
            kind=None,
1385
            category="string",
1386
            length_expr=length_expr,
1387
        )
1388
    if prop_type == "integer":
1✔
1389
        kind = prop.get("x-fortran-kind")
1✔
1390
        if kind is None:
1✔
1391
            return ScalarTypeInfo(
1✔
1392
                type_spec="integer",
1393
                arg_type_spec="integer",
1394
                kind=None,
1395
                category="integer",
1396
            )
1397
        if not isinstance(kind, str) or not kind.strip():
1✔
1398
            raise ValueError("integer property 'x-fortran-kind' must be a non-empty string")
×
1399
        return ScalarTypeInfo(
1✔
1400
            type_spec=f"integer({kind})",
1401
            arg_type_spec=f"integer({kind})",
1402
            kind=kind,
1403
            category="integer",
1404
        )
1405
    if prop_type == "number":
1✔
1406
        kind = prop.get("x-fortran-kind")
1✔
1407
        if kind is None:
1✔
1408
            return ScalarTypeInfo(
1✔
1409
                type_spec="real",
1410
                arg_type_spec="real",
1411
                kind=None,
1412
                category="real",
1413
            )
1414
        if not isinstance(kind, str) or not kind.strip():
1✔
1415
            raise ValueError("number property 'x-fortran-kind' must be a non-empty string")
×
1416
        return ScalarTypeInfo(
1✔
1417
            type_spec=f"real({kind})",
1418
            arg_type_spec=f"real({kind})",
1419
            kind=kind,
1420
            category="real",
1421
        )
1422
    if prop_type == "boolean":
1✔
1423
        return ScalarTypeInfo(
1✔
1424
            type_spec="logical",
1425
            arg_type_spec="logical",
1426
            kind=None,
1427
            category="boolean",
1428
        )
1429
    raise ValueError(f"unsupported property type '{prop_type}'")
×
1430

1431

1432
def _extract_dimensions(prop: dict[str, Any]) -> list[str]:
1✔
1433
    shape = prop.get("x-fortran-shape")
1✔
1434
    if isinstance(shape, bool):
1✔
1435
        raise ValueError("array property 'x-fortran-shape' must not be a boolean")
×
1436
    if isinstance(shape, int):
1✔
1437
        return [str(shape)]
1✔
1438
    if isinstance(shape, str):
1✔
1439
        dim = shape.strip()
1✔
1440
        if not dim:
1✔
1441
            raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
1442
        _validate_dimension_token(dim)
1✔
1443
        return [dim]
1✔
1444
    if isinstance(shape, list):
1✔
1445
        dimensions: list[str] = []
1✔
1446
        for dim in shape:
1✔
1447
            if isinstance(dim, bool):
1✔
1448
                raise ValueError("array property 'x-fortran-shape' must not include booleans")
×
1449
            if isinstance(dim, int):
1✔
1450
                dim_literal = str(dim)
1✔
1451
            elif isinstance(dim, str):
1✔
1452
                dim_literal = dim.strip()
1✔
1453
                if not dim_literal:
1✔
1454
                    raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
1455
            else:
1456
                raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
1457
            _validate_dimension_token(dim_literal)
1✔
1458
            dimensions.append(dim_literal)
1✔
1459
        return dimensions
1✔
1460
    if shape is None:
1✔
1461
        raise ValueError("array property must define 'x-fortran-shape'")
1✔
1462
    raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
1463

1464

1465
def _is_int_literal(value: str) -> bool:
1✔
1466
    try:
1✔
1467
        int(value)
1✔
1468
    except ValueError:
1✔
1469
        return False
1✔
1470
    return True
1✔
1471

1472

1473
def _validate_dimension_token(dim: str) -> None:
1✔
1474
    if dim == ":":
1✔
1475
        return
×
1476
    if _is_int_literal(dim):
1✔
1477
        return
1✔
1478
    if FORTRAN_IDENTIFIER.match(dim):
1✔
1479
        return
1✔
1480
    raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
1481

1482

1483
def _validate_length_token(length_expr: str) -> None:
1✔
1484
    if _is_int_literal(length_expr):
1✔
1485
        return
×
1486
    if FORTRAN_IDENTIFIER.match(length_expr):
1✔
1487
        return
1✔
1488
    raise ValueError("string length must be an integer literal or identifier")
×
1489

1490

1491
def _parse_flex_dim(prop: dict[str, Any], type_info: FieldTypeInfo) -> int:
1✔
1492
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
1493
    if flex_raw is None:
1✔
1494
        flex_value = 0
1✔
1495
    else:
1496
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
1✔
1497
            raise ValueError("x-fortran-flex-tail-dims must be an integer")
×
1498
        flex_value = flex_raw
1✔
1499
    if flex_value < 0:
1✔
1500
        raise ValueError("x-fortran-flex-tail-dims must be >= 0")
×
1501
    if flex_value == 0:
1✔
1502
        return 0
1✔
1503
    if type_info.category != "array":
1✔
1504
        raise ValueError("x-fortran-flex-tail-dims is only supported for arrays")
1✔
1505
    if flex_value > len(type_info.dimensions):
1✔
1506
        raise ValueError("x-fortran-flex-tail-dims must not exceed array rank")
1✔
1507
    if any(dim == ":" for dim in type_info.dimensions):
1✔
1508
        raise ValueError(
×
1509
            "x-fortran-flex-tail-dims does not support deferred-size dimensions"
1510
        )
1511
    return flex_value
1✔
1512

1513

1514
def _collect_dimension_constants(
1✔
1515
    dimensions: list[str],
1516
    constants: dict[str, int] | None,
1517
) -> list[str]:
1518
    used: list[str] = []
1✔
1519
    for dim in dimensions:
1✔
1520
        if dim == ":" or _is_int_literal(dim):
1✔
1521
            continue
1✔
1522
        if not FORTRAN_IDENTIFIER.match(dim):
1✔
1523
            raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
1524
        dim_key = dim.lower()
1✔
1525
        if constants is None or dim_key not in constants:
1✔
1526
            raise ValueError(f"dimension constant '{dim}' is not defined in config")
1✔
1527
        value = constants[dim_key]
1✔
1528
        if isinstance(value, bool) or not isinstance(value, int):
1✔
1529
            raise ValueError(f"dimension constant '{dim}' must be an integer")
×
1530
        if dim not in used:
1✔
1531
            used.append(dim)
1✔
1532
    return used
1✔
1533

1534

1535
def _render_declaration(type_spec: str, dimensions: list[str], name: str) -> str:
1✔
1536
    parts = [type_spec]
1✔
1537
    if dimensions:
1✔
1538
        dims = ", ".join(dimensions)
1✔
1539
        parts.append(f"dimension({dims})")
1✔
1540
    return f"{', '.join(parts)} :: {name}"
1✔
1541

1542

1543
def _render_runtime_declaration(
1✔
1544
    type_info: FieldTypeInfo,
1545
    name: str,
1546
) -> str:
1547
    if type_info.category == "string":
1✔
1548
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
1549
    if type_info.category != "array":
1✔
1550
        raise ValueError(
×
1551
            "runtime declaration is only supported for arrays, "
1552
            f"got category '{type_info.category}'"
1553
        )
1554

1555
    dims = ", ".join(":" for _ in type_info.dimensions)
1✔
1556
    return f"{type_info.type_spec}, allocatable, dimension({dims}) :: {name}"
1✔
1557

1558

1559
def _render_runtime_local_declaration(
1✔
1560
    type_info: FieldTypeInfo,
1561
    name: str,
1562
    *,
1563
    runtime_dimensions: list[str],
1564
) -> str:
1565
    if type_info.category == "string":
1✔
1566
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
1567
    if type_info.category != "array":
1✔
1568
        raise ValueError(
×
1569
            "runtime local declaration is only supported for arrays, "
1570
            f"got category '{type_info.category}'"
1571
        )
1572

1573
    type_spec = type_info.type_spec
1✔
1574
    dims = ", ".join(":" for _ in runtime_dimensions)
1✔
1575
    return f"{type_spec}, allocatable, dimension({dims}) :: {name}"
1✔
1576

1577

1578
def _render_runtime_allocations(
1✔
1579
    type_info: FieldTypeInfo,
1580
    name: str,
1581
    *,
1582
    runtime_dimensions: list[str],
1583
    runtime_length_expr: str | None,
1584
    target_prefix: str = "",
1585
) -> list[str]:
1586
    lines: list[str] = []
1✔
1587
    target_ref = f"{target_prefix}{name}"
1✔
1588
    if type_info.category != "array":
1✔
1589
        raise ValueError("runtime allocation is only supported for arrays")
×
1590
    if any(dim == ":" for dim in runtime_dimensions):
1✔
1591
        raise ValueError("runtime-sized arrays do not support deferred-size dimensions")
×
1592

1593
    lines.append(f"if (allocated({target_ref})) deallocate({target_ref})")
1✔
1594
    dims_expr = ", ".join(runtime_dimensions)
1✔
1595
    if type_info.element_category == "string" and runtime_length_expr is not None:
1✔
1596
        lines.append(f"allocate(character(len={runtime_length_expr}) :: {target_ref}({dims_expr}))")
1✔
1597
    else:
1598
        lines.append(f"allocate({target_ref}({dims_expr}))")
1✔
1599
    return lines
1✔
1600

1601

1602
def _render_runtime_deallocations(name: str, *, target_prefix: str = "") -> list[str]:
1✔
1603
    target_ref = f"{target_prefix}{name}"
1✔
1604
    return [f"if (allocated({target_ref})) deallocate({target_ref})"]
1✔
1605

1606

1607
def _render_runtime_local_allocations(
1✔
1608
    type_info: FieldTypeInfo,
1609
    name: str,
1610
    *,
1611
    runtime_dimensions: list[str],
1612
    runtime_length_expr: str | None,
1613
) -> list[str]:
1614
    return _render_runtime_allocations(
1✔
1615
        type_info,
1616
        name,
1617
        runtime_dimensions=runtime_dimensions,
1618
        runtime_length_expr=runtime_length_expr,
1619
    )
1620

1621

1622
def _array_section_ref(base_ref: str, rank: int) -> str:
1✔
1623
    dims = ", ".join(":" for _ in range(rank))
1✔
1624
    return f"{base_ref}({dims})"
1✔
1625

1626

1627
def _render_argument_declaration(
1✔
1628
    *,
1629
    name: str,
1630
    type_info: FieldTypeInfo,
1631
    is_required: bool,
1632
    dimensions: list[str] | None = None,
1633
    doc: str | None = None,
1634
) -> str:
1635
    intent = "intent(in)"
1✔
1636
    parts = [type_info.arg_type_spec]
1✔
1637
    arg_dimensions = dimensions if dimensions is not None else type_info.dimensions
1✔
1638
    if arg_dimensions:
1✔
1639
        dims = ", ".join(arg_dimensions)
1✔
1640
        parts.append(f"dimension({dims})")
1✔
1641
    if not is_required:
1✔
1642
        parts.append(intent)
1✔
1643
        parts.append("optional")
1✔
1644
        decl = f"{', '.join(parts[:-1])}, {parts[-1]} :: {name}"
1✔
1645
    else:
1646
        parts.append(intent)
1✔
1647
        decl = f"{', '.join(parts)} :: {name}"
1✔
1648
    if doc:
1✔
1649
        decl = f"{decl} !< {doc}"
1✔
1650
    return decl
1✔
1651

1652

1653
def _sentinel_comment(type_info: FieldTypeInfo, *, required: bool) -> str:
1✔
1654
    label = "required" if required else "optional"
1✔
1655
    category = type_info.category
1✔
1656
    if category == "array":
1✔
1657
        element = type_info.element_category or "array"
1✔
1658
        return f" ! sentinel for {label} {element} array"
1✔
1659
    if category == "string":
1✔
1660
        if required:
1✔
1661
            return " ! NULL string as sentinel for required string"
1✔
1662
        return " ! sentinel for optional string"
1✔
1663
    if category == "integer":
1✔
1664
        return f" ! sentinel for {label} integer"
1✔
1665
    if category == "real":
1✔
1666
        return f" ! sentinel for {label} real"
1✔
1667
    return ""
×
1668

1669

1670
def _render_sentinel_assignment(
1✔
1671
    type_info: FieldTypeInfo,
1672
    *,
1673
    target_ref: str,
1674
    value_expr: str,
1675
    comment: str,
1676
) -> str:
1677
    if type_info.category == "string":
1✔
1678
        return _render_string_sentinel_assignment(
1✔
1679
            target_ref=target_ref,
1680
            value_expr=value_expr,
1681
            comment=comment,
1682
        )
1683
    if type_info.category == "array" and type_info.element_category == "string":
1✔
1684
        return _render_string_array_sentinel_assignment(
1✔
1685
            target_ref=target_ref,
1686
            value_expr=value_expr,
1687
            comment=comment,
1688
        )
1689
    return f"{target_ref} = {value_expr}{comment}"
1✔
1690

1691

1692
def _render_string_sentinel_assignment(
1✔
1693
    *,
1694
    target_ref: str,
1695
    value_expr: str,
1696
    comment: str,
1697
) -> str:
1698
    return f"{target_ref} = {value_expr}{comment}"
1✔
1699

1700

1701
def _render_string_array_sentinel_assignment(
1✔
1702
    *,
1703
    target_ref: str,
1704
    value_expr: str,
1705
    comment: str,
1706
) -> str:
1707
    return f"{target_ref} = {value_expr}{comment}"
1✔
1708

1709

1710
def _sentinel_expressions(
1✔
1711
    type_info: FieldTypeInfo,
1712
    *,
1713
    var_ref: str,
1714
    len_ref: str | None = None,
1715
) -> tuple[str, str, bool]:
1716
    category = type_info.category
1✔
1717
    if category == "array":
1✔
1718
        element = type_info.element_category
1✔
1719
        if element == "string":
1✔
1720
            return "achar(0)", f"all({var_ref} == achar(0))", False
1✔
1721
        if element == "integer":
1✔
1722
            return f"-huge({var_ref})", f"all({var_ref} == -huge({var_ref}))", False
1✔
1723
        if element == "real":
1✔
1724
            return (
1✔
1725
                f"ieee_value({var_ref}, ieee_quiet_nan)",
1726
                f"all(ieee_is_nan({var_ref}))",
1727
                True,
1728
            )
1729
        if element == "boolean":
×
1730
            raise ValueError("boolean arrays cannot use sentinels")
×
1731
        raise ValueError(f"unsupported sentinel array element '{element}'")
×
1732
    if category == "string":
1✔
1733
        return "achar(0)", f"{var_ref} == achar(0)", False
1✔
1734
    if category == "integer":
1✔
1735
        return f"-huge({var_ref})", f"{var_ref} == -huge({var_ref})", False
1✔
1736
    if category == "real":
1✔
1737
        return (
1✔
1738
            f"ieee_value({var_ref}, ieee_quiet_nan)",
1739
            f"ieee_is_nan({var_ref})",
1740
            True,
1741
        )
1742
    if category == "boolean":
×
1743
        raise ValueError("boolean values cannot use sentinels")
×
1744
    raise ValueError(f"unsupported sentinel category '{category}'")
×
1745

1746

1747
def _element_missing_expression(
1✔
1748
    category: str,
1749
    *,
1750
    var_ref: str,
1751
    len_ref: str | None = None,
1752
) -> tuple[str, bool]:
1753
    if category == "string":
1✔
1754
        return f"{var_ref} == achar(0)", False
×
1755
    if category == "integer":
1✔
1756
        return f"{var_ref} == -huge({var_ref})", False
1✔
1757
    if category == "real":
1✔
1758
        return f"ieee_is_nan({var_ref})", True
1✔
1759
    raise ValueError(f"unsupported missing category '{category}'")
×
1760

1761

1762
def _array_missing_conditions(
1✔
1763
    element_category: str,
1764
    *,
1765
    var_ref: str,
1766
    len_ref: str | None = None,
1767
) -> tuple[str, str, bool]:
1768
    missing_expr, uses_ieee = _element_missing_expression(
1✔
1769
        element_category, var_ref=var_ref, len_ref=len_ref
1770
    )
1771
    return f"all({missing_expr})", f"any({missing_expr})", uses_ieee
1✔
1772

1773

1774
def _slice_ref(name: str, rank: int, dim: int, index_var: str) -> str:
1✔
1775
    dims = [":" for _ in range(rank)]
1✔
1776
    dims[dim - 1] = index_var
1✔
1777
    return f"this%{name}({', '.join(dims)})"
1✔
1778

1779

1780
def _flex_bound_vars(dim: int) -> tuple[str, str]:
1✔
1781
    return f"lb_{dim}", f"ub_{dim}"
1✔
1782

1783

1784
def _slice_ref_bounds(
1✔
1785
    name: str,
1786
    rank: int,
1787
    flex_dims: list[int],
1788
    lb_vars: dict[int, str],
1789
    ub_vars: dict[int, str],
1790
) -> str:
1791
    dims: list[str] = []
1✔
1792
    for dim in range(1, rank + 1):
1✔
1793
        if dim in flex_dims:
1✔
1794
            dims.append(f"{lb_vars[dim]}:{ub_vars[dim]}")
1✔
1795
        else:
1796
            dims.append(":")
1✔
1797
    return f"this%{name}({', '.join(dims)})"
1✔
1798

1799

1800
def _render_partial_set_block(
1✔
1801
    name: str,
1802
    rank: int,
1803
    bounds: list[dict[str, Any]],
1804
) -> str:
1805
    lb_vars = {entry["dim"]: entry["lb_var"] for entry in bounds}
1✔
1806
    ub_vars = {entry["dim"]: entry["ub_var"] for entry in bounds}
1✔
1807
    dims_all = [entry["dim"] for entry in bounds]
1✔
1808
    lines: list[str] = []
1✔
1809
    for entry in bounds:
1✔
1810
        dim = entry["dim"]
1✔
1811
        lb_var = entry["lb_var"]
1✔
1812
        ub_var = entry["ub_var"]
1✔
1813
        lines.append(f"if (size({name}, {dim}) > size(this%{name}, {dim})) then")
1✔
1814
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
1815
        lines.append(
1✔
1816
            f"  if (present(errmsg)) errmsg = \"dimension {dim} exceeds bounds for '{name}'\""
1817
        )
1818
        lines.append("  return")
1✔
1819
        lines.append("end if")
1✔
1820
        lines.append(f"{lb_var} = lbound(this%{name}, {dim})")
1✔
1821
        lines.append(f"{ub_var} = {lb_var} + size({name}, {dim}) - 1")
1✔
1822
    target_ref = _slice_ref_bounds(name, rank, dims_all, lb_vars, ub_vars)
1✔
1823
    lines.append(f"{target_ref} = {name}")
1✔
1824
    return "\n".join(lines)
1✔
1825

1826

1827
def _sort_bound_vars(values: set[str]) -> list[str]:
1✔
1828
    def sort_key(name: str) -> tuple[str, int]:
1✔
1829
        prefix, _, suffix = name.partition("_")
1✔
1830
        try:
1✔
1831
            return prefix, int(suffix)
1✔
1832
        except ValueError:
×
1833
            return prefix, 0
×
1834

1835
    return sorted(values, key=sort_key)
1✔
1836

1837

1838
def _format_reshape_assignment(name: str, arguments: list[str]) -> str:
1✔
1839
    lines = [f"this%{name} = reshape( &"]
1✔
1840
    for index, arg in enumerate(arguments):
1✔
1841
        suffix = ", &" if index < len(arguments) - 1 else ")"
1✔
1842
        lines.append(f"  {arg}{suffix}")
1✔
1843
    return "\n".join(lines)
1✔
1844

1845

1846
def _format_default(
1✔
1847
    value: Any,
1848
    type_info: FieldTypeInfo,
1849
    prop: dict[str, Any],
1850
    constants: dict[str, int] | None = None,
1851
) -> str:
1852
    if type_info.category == "array":
1✔
1853
        if not isinstance(value, list):
×
1854
            raise ValueError("array default must be a list")
×
1855
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
1856
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
1857
        elements = [
×
1858
            _format_scalar_default(element, type_info.kind, type_info.element_category)
1859
            for element in array_default.source_values
1860
        ]
1861
        if (
×
1862
            len(type_info.dimensions) == 1
1863
            and array_default.order_values is None
1864
            and array_default.pad_values is None
1865
        ):
1866
            return f"[{', '.join(elements)}]"
×
1867

1868
        shape_literal = ", ".join(type_info.dimensions)
×
1869
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
1870

1871
        if array_default.order_values is not None:
×
1872
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
1873
            arguments.append(f"order=[{order_literal}]")
×
1874

1875
        if array_default.pad_values is not None:
×
1876
            pad_elements = [
×
1877
                _format_scalar_default(element, type_info.kind, type_info.element_category)
1878
                for element in array_default.pad_values
1879
            ]
1880
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
1881

1882
        return f"reshape({', '.join(arguments)})"
×
1883
    return _format_scalar_default(value, type_info.kind, type_info.category)
1✔
1884

1885

1886
def _format_scalar_default(value: Any, kind: str | None, category: str | None) -> str:
1✔
1887
    if category == "integer":
1✔
1888
        if not isinstance(value, int):
1✔
1889
            raise ValueError("integer default must be an int")
×
1890
        suffix = f"_{kind}" if kind else ""
1✔
1891
        return f"{value}{suffix}"
1✔
1892
    if category == "real":
1✔
1893
        number = float(value)
1✔
1894
        literal = repr(number)
1✔
1895
        if literal.lower() == "nan":
1✔
1896
            raise ValueError("NaN defaults are not supported")
×
1897
        if "E" in literal:
1✔
1898
            literal = literal.replace("E", "e")
×
1899
        if "." not in literal and "e" not in literal:
1✔
1900
            literal = f"{literal}.0"
×
1901
        suffix = f"_{kind}" if kind else ""
1✔
1902
        return f"{literal}{suffix}"
1✔
1903
    if category == "boolean":
1✔
1904
        if not isinstance(value, bool):
1✔
1905
            raise ValueError("boolean default must be a bool")
×
1906
        return ".true." if value else ".false."
1✔
1907
    if category == "string":
1✔
1908
        if not isinstance(value, str):
1✔
1909
            raise ValueError("string default must be a str")
×
1910
        escaped = value.replace('"', '""')
1✔
1911
        return f'"{escaped}"'
1✔
1912
    raise ValueError(f"unsupported default category '{category}'")
×
1913

1914

1915
def _parse_default_dimensions(
1✔
1916
    dimensions: list[str],
1917
    constants: dict[str, int] | None,
1918
) -> list[int]:
1919
    if not dimensions:
1✔
1920
        raise ValueError("array property missing dimensions")
×
1921
    parsed: list[int] = []
1✔
1922
    for dim in dimensions:
1✔
1923
        if dim == ":":
1✔
1924
            raise ValueError("defaults not supported for deferred-size dimensions")
×
1925
        try:
1✔
1926
            parsed.append(int(dim))
1✔
1927
        except (TypeError, ValueError) as err:  # pragma: no cover - defensive
1928
            dim_key = dim.lower()
1929
            if constants is None or dim_key not in constants:
1930
                raise ValueError(
1931
                    "array default dimensions must be integer literals or defined constants"
1932
                ) from err
1933
            value = constants[dim_key]
1934
            if isinstance(value, bool) or not isinstance(value, int):
1935
                raise ValueError("array default dimension constants must be integers") from err
1936
            parsed.append(value)
1937
    return parsed
1✔
1938

1939

1940
def _prepare_array_default(
1✔
1941
    value: list[Any],
1942
    dims: list[int],
1943
    prop: dict[str, Any],
1944
) -> ArrayDefaultSpec:
1945
    default_values = _ensure_flat_scalar_list(value, "array default")
1✔
1946
    if not default_values:
1✔
1947
        raise ValueError("array default must contain at least one value")
×
1948

1949
    total_size = math.prod(dims)
1✔
1950
    if len(default_values) > total_size:
1✔
1951
        raise ValueError("array default longer than declared x-fortran-shape")
×
1952

1953
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
1954
    if not isinstance(order_raw, str):
1✔
1955
        raise ValueError("array default order must be 'F' or 'C'")
×
1956
    order = order_raw.upper()
1✔
1957
    if order not in {"F", "C"}:
1✔
1958
        raise ValueError("array default order must be 'F' or 'C'")
×
1959

1960
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
1961
    if not isinstance(repeat_raw, bool):
1✔
1962
        raise ValueError("array default repeat must be a boolean")
×
1963
    repeat = bool(repeat_raw)
1✔
1964

1965
    pad_raw = prop.get("x-fortran-default-pad")
1✔
1966
    pad_values: list[Any] | None = None
1✔
1967
    if pad_raw is not None:
1✔
1968
        if repeat:
1✔
1969
            raise ValueError("array default cannot set both pad and repeat")
×
1970
        if not isinstance(pad_raw, list):
1✔
1971
            pad_raw = [pad_raw]
1✔
1972
        pad_values = _ensure_flat_scalar_list(pad_raw, "array default pad")
1✔
1973
        if not pad_values:
1✔
1974
            raise ValueError("array default pad must contain at least one value")
×
1975

1976
    if len(default_values) < total_size and pad_values is None and not repeat:
1✔
UNCOV
1977
        raise ValueError(
×
1978
            "array default shorter than declared x-fortran-shape without pad or repeat"
1979
        )
1980

1981
    if repeat:
1✔
1982
        pad_values = list(default_values)
1✔
1983
        if not pad_values:
1✔
1984
            raise ValueError("array default repeat requires at least one value")
×
1985

1986
    order_values: list[int] | None = None
1✔
1987
    if order == "C" and len(dims) > 1:
1✔
1988
        rank = len(dims)
1✔
1989
        order_values = list(range(rank, 0, -1))
1✔
1990

1991
    return ArrayDefaultSpec(
1✔
1992
        source_values=list(default_values),
1993
        pad_values=pad_values,
1994
        order_values=order_values,
1995
    )
1996

1997

1998
def _ensure_flat_scalar_list(values: list[Any], description: str) -> list[Any]:
1✔
1999
    normalized: list[Any] = []
1✔
2000
    for element in values:
1✔
2001
        if isinstance(element, list):
1✔
2002
            raise ValueError(f"{description} must be a flat list")
×
2003
        normalized.append(element)
1✔
2004
    return normalized
1✔
2005

2006

2007
def _enum_values(
1✔
2008
    prop: dict[str, Any],
2009
    type_info: FieldTypeInfo,
2010
    constants: dict[str, int] | None,
2011
) -> list[Any] | None:
2012
    if type_info.category == "array":
1✔
2013
        enum_raw = _array_items_enum(prop)
1✔
2014
    else:
2015
        enum_raw = prop.get("enum")
1✔
2016
    if enum_raw is None:
1✔
2017
        return None
1✔
2018
    if not isinstance(enum_raw, list) or not enum_raw:
1✔
2019
        raise ValueError("property enum must be a non-empty list")
×
2020
    enum_values = _ensure_flat_scalar_list(enum_raw, "enum")
1✔
2021
    category = _enum_category(type_info)
1✔
2022
    if category not in {"integer", "string"}:
1✔
2023
        raise ValueError("enum only supported for integer or string values")
×
2024
    for value in enum_values:
1✔
2025
        _validate_enum_scalar(value, category, "enum")
1✔
2026
    _validate_enum_defaults(prop, type_info, enum_values, category, constants)
1✔
2027
    _validate_enum_examples(prop, type_info, enum_values, category)
1✔
2028
    return enum_values
1✔
2029

2030

2031
def _bounds_spec(
1✔
2032
    prop: dict[str, Any],
2033
    type_info: FieldTypeInfo,
2034
) -> dict[str, Any] | None:
2035
    if type_info.category == "array":
1✔
2036
        bounds_prop = _array_items_bounds(prop)
1✔
2037
        category = type_info.element_category
1✔
2038
    else:
2039
        bounds_prop = prop
1✔
2040
        category = type_info.category
1✔
2041

2042
    min_value, min_exclusive = _extract_bound_value(
1✔
2043
        bounds_prop, "minimum", "exclusiveMinimum"
2044
    )
2045
    max_value, max_exclusive = _extract_bound_value(
1✔
2046
        bounds_prop, "maximum", "exclusiveMaximum"
2047
    )
2048

2049
    if min_value is None and max_value is None:
1✔
2050
        return None
1✔
2051

2052
    if category not in {"integer", "real"}:
1✔
2053
        raise ValueError("bounds only supported for integer or real values")
×
2054

2055
    if min_value is not None:
1✔
2056
        _validate_bound_scalar(min_value, category, "minimum")
1✔
2057
    if max_value is not None:
1✔
2058
        _validate_bound_scalar(max_value, category, "maximum")
1✔
2059

2060
    if min_value is not None and max_value is not None:
1✔
2061
        min_comp = float(min_value) if category == "real" else int(min_value)
1✔
2062
        max_comp = float(max_value) if category == "real" else int(max_value)
1✔
2063
        if min_exclusive or max_exclusive:
1✔
2064
            if min_comp >= max_comp:
1✔
2065
                raise ValueError("minimum must be less than maximum for exclusive bounds")
×
2066
        else:
2067
            if min_comp > max_comp:
×
2068
                raise ValueError("minimum must be <= maximum")
×
2069

2070
    return {
1✔
2071
        "min_value": min_value,
2072
        "min_exclusive": min_exclusive,
2073
        "max_value": max_value,
2074
        "max_exclusive": max_exclusive,
2075
        "category": category,
2076
    }
2077

2078

2079
def _extract_bound_value(
1✔
2080
    prop: dict[str, Any],
2081
    inclusive_key: str,
2082
    exclusive_key: str,
2083
) -> tuple[Any | None, bool]:
2084
    has_inclusive = inclusive_key in prop
1✔
2085
    has_exclusive = exclusive_key in prop
1✔
2086
    if has_inclusive and has_exclusive:
1✔
2087
        raise ValueError(
×
2088
            f"property must not define both '{inclusive_key}' and '{exclusive_key}'"
2089
        )
2090
    if has_exclusive:
1✔
2091
        value = prop.get(exclusive_key)
1✔
2092
        if value is None:
1✔
2093
            raise ValueError(f"{exclusive_key} must be a number")
×
2094
        return value, True
1✔
2095
    if has_inclusive:
1✔
2096
        value = prop.get(inclusive_key)
1✔
2097
        if value is None:
1✔
2098
            raise ValueError(f"{inclusive_key} must be a number")
×
2099
        return value, False
1✔
2100
    return None, False
1✔
2101

2102

2103
def _validate_bound_scalar(value: Any, category: str, label: str) -> None:
1✔
2104
    if category == "integer":
1✔
2105
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2106
            raise ValueError(f"{label} must be an integer")
×
2107
        return
1✔
2108
    if category == "real":
1✔
2109
        if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
2110
            raise ValueError(f"{label} must be a number")
×
2111
        if math.isinf(float(value)):
1✔
2112
            raise ValueError(f"{label} must not be infinite")
×
2113
        if math.isnan(float(value)):
1✔
2114
            raise ValueError(f"{label} must not be NaN")
×
2115
        return
1✔
2116
    raise ValueError("bounds only supported for integer or real values")
×
2117

2118

2119
def _array_default_value(prop: dict[str, Any]) -> tuple[Any, bool] | None:
1✔
2120
    if prop.get("type") != "array":
1✔
2121
        raise ValueError("array default lookup requires array properties")
×
2122

2123
    default_defined = "default" in prop
1✔
2124
    items_default = _array_items_default(prop)
1✔
2125
    items_defined = "default" in items_default
1✔
2126
    items_value = items_default.get("default") if items_defined else None
1✔
2127

2128
    if default_defined and items_defined:
1✔
2129
        raise ValueError("array default must be defined on property or items, not both")
×
2130

2131
    if items_defined:
1✔
2132
        for key in ("x-fortran-default-order", "x-fortran-default-repeat", "x-fortran-default-pad"):
1✔
2133
            if key in prop:
1✔
2134
                raise ValueError("array items default must not use x-fortran-default-* options")
×
2135
        if isinstance(items_value, list):
1✔
2136
            raise ValueError("array items default must be a scalar")
×
2137
        return items_value, True
1✔
2138

2139
    if default_defined:
1✔
2140
        default_value = prop.get("default")
1✔
2141
        if not isinstance(default_value, list):
1✔
2142
            raise ValueError("array default must be a list")
1✔
2143
        return default_value, False
1✔
2144
    return None
1✔
2145

2146

2147
def _array_items_enum(prop: dict[str, Any]) -> list[Any] | None:
1✔
2148
    current = prop
1✔
2149
    while current.get("type") == "array":
1✔
2150
        if "enum" in current:
1✔
2151
            raise ValueError("array enum must be defined on items")
1✔
2152
        items = current.get("items")
1✔
2153
        if not isinstance(items, dict):
1✔
2154
            raise ValueError("array property must define 'items'")
×
2155
        current = items
1✔
2156
    return current.get("enum")
1✔
2157

2158

2159
def _array_items_default(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2160
    current = prop
1✔
2161
    while current.get("type") == "array":
1✔
2162
        items = current.get("items")
1✔
2163
        if not isinstance(items, dict):
1✔
2164
            raise ValueError("array property must define 'items'")
×
2165
        if items.get("type") == "array":
1✔
2166
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
1✔
2167
        current = items
1✔
2168
    return current
1✔
2169

2170

2171
def _array_items_bounds(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2172
    current = prop
1✔
2173
    while current.get("type") == "array":
1✔
2174
        for key in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"):
1✔
2175
            if key in current:
1✔
2176
                raise ValueError("array bounds must be defined on items")
×
2177
        items = current.get("items")
1✔
2178
        if not isinstance(items, dict):
1✔
2179
            raise ValueError("array property must define 'items'")
×
2180
        if items.get("type") == "array":
1✔
2181
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2182
        current = items
1✔
2183
    return current
1✔
2184

2185

2186
def _validate_enum_scalar(value: Any, category: str, label: str) -> None:
1✔
2187
    if category == "integer":
1✔
2188
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2189
            raise ValueError(f"{label} values must be integers")
×
2190
        return
1✔
2191
    if category == "string":
1✔
2192
        if not isinstance(value, str):
1✔
2193
            raise ValueError(f"{label} values must be strings")
×
2194
        return
1✔
2195
    raise ValueError("enum only supported for integer or string values")
×
2196

2197

2198
def _ensure_enum_member(value: Any, enum_values: list[Any], category: str, label: str) -> None:
1✔
2199
    _validate_enum_scalar(value, category, label)
×
2200
    if value not in enum_values:
×
2201
        raise ValueError(f"{label} value must be one of enum values")
×
2202

2203

2204
def _validate_enum_defaults(
1✔
2205
    prop: dict[str, Any],
2206
    type_info: FieldTypeInfo,
2207
    enum_values: list[Any],
2208
    category: str,
2209
    constants: dict[str, int] | None,
2210
) -> None:
2211
    if type_info.category == "array":
1✔
2212
        default_info = _array_default_value(prop)
1✔
2213
        if default_info is not None:
1✔
2214
            default_value, default_from_items = default_info
×
2215
            if default_from_items:
×
2216
                _ensure_enum_member(default_value, enum_values, category, "default")
×
2217
            else:
2218
                default_values = _ensure_flat_scalar_list(default_value, "array default")
×
2219
                for value in default_values:
×
2220
                    _ensure_enum_member(value, enum_values, category, "default")
×
2221
        pad_raw = prop.get("x-fortran-default-pad")
1✔
2222
        if pad_raw is not None:
1✔
2223
            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
2224
            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
×
2225
            for value in pad_values:
×
2226
                _ensure_enum_member(value, enum_values, category, "pad")
×
2227
        return
1✔
2228
    if "default" not in prop:
1✔
2229
        return
1✔
2230
    default_value = prop["default"]
×
2231
    if isinstance(default_value, list):
×
2232
        raise ValueError("scalar default must not be a list")
×
2233
    _ensure_enum_member(default_value, enum_values, category, "default")
×
2234

2235

2236
def _validate_enum_examples(
1✔
2237
    prop: dict[str, Any],
2238
    type_info: FieldTypeInfo,
2239
    enum_values: list[Any],
2240
    category: str,
2241
) -> None:
2242
    examples = prop.get("examples")
1✔
2243
    if examples is None:
1✔
2244
        return
1✔
2245
    if not isinstance(examples, list):
×
2246
        raise ValueError("property examples must be a list")
×
2247
    for example in examples:
×
2248
        if type_info.category == "array":
×
2249
            if isinstance(example, list):
×
2250
                values = _ensure_flat_scalar_list(example, "array examples")
×
2251
                for value in values:
×
2252
                    _ensure_enum_member(value, enum_values, category, "example")
×
2253
            else:
2254
                _ensure_enum_member(example, enum_values, category, "example")
×
2255
        else:
2256
            if isinstance(example, list):
×
2257
                raise ValueError("scalar examples must not be lists")
×
2258
            _ensure_enum_member(example, enum_values, category, "example")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc