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

MuellerSeb / nml-tools / 26195436091

20 May 2026 11:14PM UTC coverage: 79.224% (+0.1%) from 79.105%
26195436091

Pull #28

github

web-flow
Merge 0cf10eaf4 into f04779747
Pull Request #28: Simplify generated internal naming with indexed clash handling

54 of 61 new or added lines in 2 files covered. (88.52%)

145 existing lines in 1 file now uncovered.

2612 of 3297 relevant lines covered (79.22%)

0.79 hits per line

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

84.25
/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

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

28

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

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

39

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

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

52

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

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

74

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

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

83

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

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

93

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

124

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

154

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

177

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

203

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

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

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

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

247
    required_fields_raw = _ordered_unique(schema.get("required", []))
1✔
248
    required_fields: list[str] = []
1✔
249
    for req_name in required_fields_raw:
1✔
250
        if not isinstance(req_name, str):
1✔
251
            raise ValueError("schema 'required' entries must be strings")
×
252
        req_key = req_name.lower()
1✔
253
        if req_key not in property_name_map:
1✔
254
            raise ValueError(f"required property '{req_name}' is not defined")
1✔
255
        if req_key not in required_fields:
1✔
256
            required_fields.append(req_key)
1✔
257
    required_set = set(required_fields)
1✔
258
    module_name = f"nml_{namelist_name}"
1✔
259
    type_name = f"{module_name}_t"
1✔
260
    doc_class = f"{module_name}_t"
1✔
261
    brief_text = schema.get("title", namelist_name)
1✔
262
    details_text = schema.get("description", brief_text)
1✔
263

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

317
    def _helper_import_local_name(import_spec: str) -> str:
1✔
318
        if "=>" in import_spec:
1✔
UNCOV
319
            return import_spec.split("=>", 1)[0].strip()
×
320
        return import_spec.strip()
1✔
321

322
    def _helper_import_local_names() -> set[str]:
1✔
323
        return {_helper_import_local_name(existing).lower() for existing in helper_imports}
1✔
324

325
    def _add_helper_import(name: str) -> None:
1✔
326
        if not any(existing.lower() == name.lower() for existing in helper_imports):
1✔
327
            helper_imports.append(name)
1✔
328

329
    def _unique_generated_name(base_name: str, taken_names: set[str]) -> str:
1✔
330
        if base_name.lower() not in taken_names:
1✔
331
            return base_name
1✔
332
        index = 1
1✔
333
        while True:
1✔
334
            candidate = f"{base_name}_{index}"
1✔
335
            if candidate.lower() not in taken_names:
1✔
336
                return candidate
1✔
UNCOV
337
            index += 1
×
338

339
    def _unique_helper_import_alias(base_name: str) -> str:
1✔
340
        local_names = _helper_import_local_names() | set(static_constants)
1✔
341
        return _unique_generated_name(base_name, local_names)
1✔
342

343
    def _register_runtime_dimension(dim_name: str) -> str:
1✔
344
        local_name = runtime_dimension_locals.get(dim_name)
1✔
345
        if local_name is None:
1✔
346
            local_base = f"dim_{dim_name}"
1✔
347
            local_taken = set(property_name_map) | {
1✔
348
                existing.lower() for existing in runtime_dimension_locals.values()
349
            }
350
            local_name = _unique_generated_name(local_base, local_taken)
1✔
351
            runtime_dimension_locals[dim_name] = local_name
1✔
352
            default_name = _unique_helper_import_alias(f"{dim_name}_default")
1✔
353
            _add_helper_import(f"{default_name}=>{dim_name}")
1✔
354
            runtime_dimensions.append(
1✔
355
                {
356
                    "name": dim_name,
357
                    "default_name": default_name,
358
                    "local_name": local_name,
359
                }
360
            )
361
        return local_name
1✔
362

363
    current_property: str | None = None
1✔
364
    try:
1✔
365
        for index, (display_name, attr_name, prop) in enumerate(property_items):
1✔
366
            current_property = display_name
1✔
367
            name = attr_name
1✔
368
            _reject_runtime_dimension_lengths(prop, runtime_dimension_values)
1✔
369
            type_info = _field_type_info(prop, static_constants)
1✔
370
            for const_name in _collect_dimension_constants(type_info.dimensions, shape_constants):
1✔
371
                const_key = const_name.lower()
1✔
372
                if const_key in runtime_dimension_values:
1✔
373
                    _register_runtime_dimension(const_key)
1✔
374
                else:
375
                    _add_helper_import(const_name)
1✔
376
            if type_info.length_expr and not _is_int_literal(type_info.length_expr):
1✔
377
                _add_helper_import(type_info.length_expr)
1✔
378
            if type_info.kind:
1✔
379
                kind_ids.append(type_info.kind)
1✔
380

381
            runtime_shape = list(type_info.dimensions)
1✔
382
            dynamic_shape = False
1✔
383
            if type_info.category == "array":
1✔
384
                for dim_index, dim in enumerate(runtime_shape):
1✔
385
                    if dim == ":" or _is_int_literal(dim):
1✔
386
                        continue
1✔
387
                    if not FORTRAN_IDENTIFIER.match(dim):
1✔
UNCOV
388
                        raise ValueError(
×
389
                            "array property 'x-fortran-shape' entries must be ints or identifiers"
390
                        )
391
                    dim_key = dim.lower()
1✔
392
                    if dim_key in runtime_dimension_values:
1✔
393
                        local_dim_name = _register_runtime_dimension(dim_key)
1✔
394
                        runtime_shape[dim_index] = f"this%{local_dim_name}"
1✔
395
                        dynamic_shape = True
1✔
396
                    elif dim_key not in static_constants:
1✔
UNCOV
397
                        raise ValueError(f"dimension constant '{dim}' is not defined in config")
×
398

399
            runtime_length_expr = type_info.length_expr
1✔
400
            if (
1✔
401
                type_info.length_expr
402
                and not _is_int_literal(type_info.length_expr)
403
                and type_info.length_expr.lower() in runtime_dimension_values
404
            ):
UNCOV
405
                raise ValueError(
×
406
                    f"dimension '{type_info.length_expr}' cannot be used as x-fortran-len"
407
                )
408
            type_spec_with_defaults = type_info.type_spec
1✔
409

410
            array_default_info: tuple[Any, bool] | None = None
1✔
411
            if type_info.category == "array":
1✔
412
                array_default_info = _array_default_value(prop)
1✔
413

414
            flex_dim = _parse_flex_dim(prop, type_info)
1✔
415
            if flex_dim > 0:
1✔
416
                if type_info.element_category == "boolean":
1✔
417
                    raise ValueError("flex arrays cannot use boolean elements")
1✔
418
                if array_default_info is not None or any(
1✔
419
                    key in prop
420
                    for key in (
421
                        "x-fortran-default-order",
422
                        "x-fortran-default-repeat",
423
                        "x-fortran-default-pad",
424
                    )
425
                ):
426
                    raise ValueError("flex arrays cannot define defaults")
1✔
427

428
            is_runtime_sized = (
1✔
429
                type_info.category == "array" and dynamic_shape
430
            )
431

432
            if is_runtime_sized:
1✔
433
                declaration = _render_runtime_declaration(
1✔
434
                    type_info,
435
                    name,
436
                )
437
                local_decl = _render_runtime_local_declaration(
1✔
438
                    type_info,
439
                    name,
440
                    runtime_dimensions=runtime_shape,
441
                )
442
                runtime_allocations.extend(
1✔
443
                    _render_runtime_allocations(
444
                        type_info,
445
                        name,
446
                        runtime_dimensions=runtime_shape,
447
                        runtime_length_expr=runtime_length_expr,
448
                        target_prefix="this%",
449
                    )
450
                )
451
                runtime_deallocations.extend(
1✔
452
                    _render_runtime_deallocations(name, target_prefix="this%")
453
                )
454
                runtime_local_allocations.extend(
1✔
455
                    _render_runtime_local_allocations(
456
                        type_info,
457
                        name,
458
                        runtime_dimensions=runtime_shape,
459
                        runtime_length_expr=runtime_length_expr,
460
                    )
461
                )
462
            else:
463
                declaration = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
464
                local_decl = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
465

466
            title_raw = prop.get("title")
1✔
467
            if title_raw is None:
1✔
468
                title = display_name
1✔
469
            elif not isinstance(title_raw, str):
1✔
UNCOV
470
                raise ValueError(f"property '{display_name}' title must be a string")
×
471
            else:
472
                title = title_raw.strip() or name
1✔
473
            description = prop.get("description")
1✔
474
            declaration_with_doc = f"{declaration} !< {title}"
1✔
475

476
            dynamic_array = type_info.category == "array" and is_runtime_sized
1✔
477

478
            is_required = name in required_set
1✔
479
            if type_info.category == "array":
1✔
480
                has_default = array_default_info is not None
1✔
481
            else:
482
                has_default = "default" in prop
1✔
483

484
            default_from_items = False
1✔
485
            default_values: list[Any] | None = None
1✔
486
            parsed_dims: list[int] | None = None
1✔
487
            array_default_spec: ArrayDefaultSpec | None = None
1✔
488

489
            if type_info.category == "array" and has_default:
1✔
490
                if array_default_info is None:
1✔
UNCOV
491
                    raise ValueError(f"missing array default for '{display_name}'")
×
492
                default_raw, default_from_items = array_default_info
1✔
493
                if default_from_items:
1✔
494
                    if isinstance(default_raw, list):
1✔
UNCOV
495
                        raise ValueError("array items default must be a scalar")
×
496
                    default_values = [default_raw]
1✔
497
                else:
498
                    if not isinstance(default_raw, list):
1✔
UNCOV
499
                        raise ValueError("array default must be a list")
×
500
                    default_values = default_raw
1✔
501
                    parsed_dims = _parse_default_dimensions(type_info.dimensions, shape_constants)
1✔
502
                    array_default_spec = _prepare_array_default(default_values, parsed_dims, prop)
1✔
503

504
            needs_sentinel = (not is_required) and (not has_default)
1✔
505
            requires_sentinel = is_required or needs_sentinel
1✔
506

507
            arg_dimensions = type_info.dimensions
1✔
508
            if type_info.category == "array":
1✔
509
                arg_dimensions = [":" for _ in type_info.dimensions]
1✔
510
            argument_decl = _render_argument_declaration(
1✔
511
                name=name,
512
                type_info=type_info,
513
                is_required=is_required,
514
                dimensions=arg_dimensions,
515
                doc=title,
516
            )
517
            local_init_assignments.append(f"{name} = this%{name}")
1✔
518

519
            sentinel_assignment: str | None = None
1✔
520
            sentinel_condition: str | None = None
1✔
521
            set_sentinel_condition: str | None = None
1✔
522
            if requires_sentinel:
1✔
523
                if is_required and type_info.category == "boolean":
1✔
UNCOV
524
                    raise ValueError(
×
525
                        f"required {type_info.category} '{display_name}' is not supported"
526
                    )
527
                if is_required and type_info.category == "array":
1✔
528
                    if type_info.element_category == "boolean":
1✔
UNCOV
529
                        raise ValueError("required boolean arrays are not supported")
×
530
                if needs_sentinel and type_info.category == "boolean":
1✔
UNCOV
531
                    raise ValueError(f"optional boolean '{display_name}' must define a default")
×
532
                if needs_sentinel and type_info.category == "array":
1✔
533
                    if type_info.element_category == "boolean":
1✔
UNCOV
534
                        raise ValueError("optional boolean arrays must define a default")
×
535
                value_expr, condition_expr, uses_ieee = _sentinel_expressions(
1✔
536
                    type_info,
537
                    var_ref=f"this%{name}",
538
                )
539
                sentinel_assignment = _render_sentinel_assignment(
1✔
540
                    type_info,
541
                    target_ref=f"this%{name}",
542
                    value_expr=value_expr,
543
                    comment=_sentinel_comment(type_info, required=is_required),
544
                )
545
                sentinel_condition = condition_expr
1✔
546
                sentinel_assignments.append(sentinel_assignment)
1✔
547
                if uses_ieee:
1✔
548
                    requires_ieee = True
1✔
549
                if not has_default and type_info.category != "boolean":
1✔
550
                    set_value_expr, set_condition_expr, set_uses_ieee = _sentinel_expressions(
1✔
551
                        type_info,
552
                        var_ref=f"this%{name}",
553
                    )
554
                    if set_uses_ieee:
1✔
555
                        requires_ieee = True
1✔
556
                    set_sentinel_condition = set_condition_expr
1✔
557
                    if needs_sentinel:
1✔
558
                        sent_com = _sentinel_comment(type_info, required=False)
1✔
559
                        set_optional_defaults.append(
1✔
560
                            _render_sentinel_assignment(
561
                                type_info,
562
                                target_ref=f"this%{name}",
563
                                value_expr=set_value_expr,
564
                                comment=sent_com,
565
                            )
566
                        )
567

568
            if is_required and type_info.category != "array":
1✔
569
                required_scalar_names.add(name)
1✔
570

571
            if is_required and type_info.category == "array" and flex_dim == 0:
1✔
572
                element_category = type_info.element_category
1✔
573
                if element_category is None:
1✔
UNCOV
574
                    raise ValueError("array field missing element category")
×
575
                all_missing, any_missing, uses_ieee = _array_missing_conditions(
1✔
576
                    element_category,
577
                    var_ref=_array_section_ref(f"this%{name}", len(type_info.dimensions)),
578
                    len_ref=f"this%{name}",
579
                )
580
                required_array_by_name[name] = {
1✔
581
                    "name": display_name,
582
                    "attr_name": name,
583
                    "runtime_array": dynamic_array,
584
                    "all_missing_condition": all_missing,
585
                    "any_missing_condition": any_missing,
586
                }
587
                uses_partly_set = True
1✔
588
                if uses_ieee:
1✔
589
                    requires_ieee = True
1✔
590

591
            partial_bounds: list[dict[str, Any]] | None = None
1✔
592
            if type_info.category == "array":
1✔
593
                rank = len(type_info.dimensions)
1✔
594
                all_bounds = []
1✔
595
                for array_dim_index in range(1, rank + 1):
1✔
596
                    lb_var, ub_var = _flex_bound_vars(array_dim_index)
1✔
597
                    all_bounds.append(
1✔
598
                        {"dim": array_dim_index, "lb_var": lb_var, "ub_var": ub_var}
599
                    )
600
                    flex_bound_vars.add(lb_var)
1✔
601
                    flex_bound_vars.add(ub_var)
1✔
602
                partial_bounds = all_bounds
1✔
603
            if flex_dim > 0:
1✔
604
                rank = len(type_info.dimensions)
1✔
605
                element_category = type_info.element_category
1✔
606
                if element_category is None:
1✔
UNCOV
607
                    raise ValueError("array field missing element category")
×
608
                flex_dims: list[int] = list(range(rank - flex_dim + 1, rank + 1))
1✔
609
                slice_missing_conditions: list[str] = []
1✔
610
                slice_uses_ieee = False
1✔
611
                flex_dim_bounds: list[dict[str, Any]] = []
1✔
612
                lb_vars: dict[int, str] = {}
1✔
613
                ub_vars: dict[int, str] = {}
1✔
614
                for flex_dim_index in flex_dims:
1✔
615
                    lb_var, ub_var = _flex_bound_vars(flex_dim_index)
1✔
616
                    lb_vars[flex_dim_index] = lb_var
1✔
617
                    ub_vars[flex_dim_index] = ub_var
1✔
618
                    flex_dim_bounds.append(
1✔
619
                        {"dim": flex_dim_index, "lb_var": lb_var, "ub_var": ub_var}
620
                    )
621
                    flex_bound_vars.add(lb_var)
1✔
622
                    flex_bound_vars.add(ub_var)
1✔
623
                    slice_ref = _slice_ref(name, rank, flex_dim_index, "idx")
1✔
624
                    slice_missing_expr, uses_ieee = _element_missing_expression(
1✔
625
                        element_category,
626
                        var_ref=slice_ref,
627
                        len_ref=f"this%{name}",
628
                    )
629
                    slice_missing_conditions.append(
1✔
630
                        f"all({slice_missing_expr})" if rank > 1 else slice_missing_expr
631
                    )
632
                    slice_uses_ieee = slice_uses_ieee or uses_ieee
1✔
633
                prefix_ref = _slice_ref_bounds(name, rank, flex_dims, lb_vars, ub_vars)
1✔
634
                prefix_missing_expr, uses_ieee_prefix = _element_missing_expression(
1✔
635
                    element_category,
636
                    var_ref=prefix_ref,
637
                    len_ref=f"this%{name}",
638
                )
639
                prefix_any_missing_condition = f"any({prefix_missing_expr})"
1✔
640
                flex_arrays.append(
1✔
641
                    {
642
                        "name": name,
643
                        "display_name": display_name,
644
                        "rank": rank,
645
                        "flex_dims": flex_dims,
646
                        "required": is_required,
647
                        "runtime_array": dynamic_array,
648
                        "bounds": flex_dim_bounds,
649
                        "slice_missing_conditions": slice_missing_conditions,
650
                        "prefix_any_missing_condition": prefix_any_missing_condition,
651
                    }
652
                )
653
                uses_partly_set = True
1✔
654
                if slice_uses_ieee or uses_ieee_prefix:
1✔
UNCOV
655
                    requires_ieee = True
×
656

657
            default_assignment: str | None = None
1✔
658
            set_default_assignment: str | None = None
1✔
659
            if has_default and is_required:
1✔
UNCOV
660
                raise ValueError(f"required property '{display_name}' cannot define a default")
×
661
            if has_default:
1✔
662
                default_const_name = f"{name}_default"
1✔
663
                if type_info.category == "array":
1✔
664
                    if default_values is None:
1✔
UNCOV
665
                        raise ValueError(f"missing array default for '{display_name}'")
×
666
                    default_is_scalar = default_from_items
1✔
667

668
                    repeat = False
1✔
669
                    pad_raw = None
1✔
670
                    pad_const_name: str | None = None
1✔
671
                    pad_is_scalar = False
1✔
672

673
                    if not default_from_items:
1✔
674
                        repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
675
                        if not isinstance(repeat_raw, bool):
1✔
UNCOV
676
                            raise ValueError("array default repeat must be a boolean")
×
677
                        repeat = bool(repeat_raw)
1✔
678
                        pad_raw = prop.get("x-fortran-default-pad")
1✔
679
                        if pad_raw is not None:
1✔
680
                            pad_const_name = f"{name}_pad"
1✔
681
                            pad_is_scalar = not isinstance(pad_raw, list)
1✔
682
                            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
1✔
683
                            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
1✔
684
                            pad_elements = [
1✔
685
                                _format_scalar_default(
686
                                    element, type_info.kind, type_info.element_category
687
                                )
688
                                for element in pad_values
689
                            ]
690
                            if pad_is_scalar:
1✔
691
                                pad_literal = _format_scalar_default(
1✔
692
                                    pad_raw, type_info.kind, type_info.element_category
693
                                )
694
                                default_parameters.append(
1✔
695
                                    f"{type_spec_with_defaults}, parameter, public :: "
696
                                    f"{pad_const_name} = {pad_literal}"
697
                                )
698
                            else:
UNCOV
699
                                default_parameters.append(
×
700
                                    f"{type_spec_with_defaults}, parameter, public :: "
701
                                    f"{pad_const_name}({len(pad_elements)}) = "
702
                                    f"[{', '.join(pad_elements)}]"
703
                                )
704

705
                        if parsed_dims is None:
1✔
UNCOV
706
                            parsed_dims = _parse_default_dimensions(
×
707
                                type_info.dimensions, shape_constants
708
                            )
709
                        if array_default_spec is None:
1✔
UNCOV
710
                            array_default_spec = _prepare_array_default(
×
711
                                default_values, parsed_dims, prop
712
                            )
713

714
                    if default_is_scalar:
1✔
715
                        default_literal = _format_scalar_default(
1✔
716
                            default_values[0], type_info.kind, type_info.element_category
717
                        )
718
                        default_parameters.append(
1✔
719
                            f"{type_spec_with_defaults}, parameter, public :: "
720
                            f"{default_const_name} = {default_literal}"
721
                        )
722
                    else:
723
                        if array_default_spec is None:
1✔
UNCOV
724
                            raise ValueError(
×
725
                                f"missing array default specification for '{display_name}'"
726
                            )
727
                        default_elements = [
1✔
728
                            _format_scalar_default(
729
                                element, type_info.kind, type_info.element_category
730
                            )
731
                            for element in array_default_spec.source_values
732
                        ]
733
                        default_parameters.append(
1✔
734
                            f"{type_spec_with_defaults}, parameter, public :: "
735
                            f"{default_const_name}({len(default_elements)}) = "
736
                            f"[{', '.join(default_elements)}]"
737
                        )
738

739
                    if default_from_items:
1✔
740
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
741
                    elif (
1✔
742
                        len(type_info.dimensions) == 1
743
                        and array_default_spec is not None
744
                        and array_default_spec.order_values is None
745
                        and array_default_spec.pad_values is None
746
                    ):
747
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
748
                    else:
749
                        source_expr = default_const_name
1✔
750
                        shape_expr = ", ".join(runtime_shape)
1✔
751
                        arguments = [source_expr, f"shape=[{shape_expr}]"]
1✔
752
                        if array_default_spec is not None:
1✔
753
                            if array_default_spec.order_values is not None:
1✔
754
                                order_literal = ", ".join(
1✔
755
                                    str(index) for index in array_default_spec.order_values
756
                                )
757
                                arguments.append(f"order=[{order_literal}]")
1✔
758
                            if array_default_spec.pad_values is not None:
1✔
759
                                if repeat:
1✔
760
                                    pad_expr = default_const_name
1✔
761
                                else:
762
                                    if pad_const_name is None:
1✔
UNCOV
763
                                        raise ValueError(
×
764
                                            f"missing pad values for array default '{display_name}'"
765
                                        )
766
                                    pad_expr = (
1✔
767
                                        pad_const_name
768
                                        if not pad_is_scalar
769
                                        else f"[{pad_const_name}]"
770
                                    )
771
                                arguments.append(f"pad={pad_expr}")
1✔
772
                        default_assignment = _format_reshape_assignment(name, arguments)
1✔
773
                    set_default_assignment = default_assignment
1✔
774
                else:
775
                    default_literal = _format_default(prop["default"], type_info, prop, constants)
1✔
776
                    default_parameters.append(
1✔
777
                        f"{type_spec_with_defaults}, parameter, public :: "
778
                        f"{default_const_name} = {default_literal}"
779
                    )
780
                    if type_info.category == "boolean":
1✔
781
                        default_assignment = (
1✔
782
                            f"this%{name} = {default_const_name} "
783
                            "! bool values always need a default"
784
                        )
785
                    else:
786
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
787
                    set_default_assignment = f"this%{name} = {default_const_name}"
1✔
788

789
                if default_assignment is None or set_default_assignment is None:
1✔
UNCOV
790
                    raise ValueError(f"missing default assignment for '{display_name}'")
×
791
                default_assignments.append(default_assignment)
1✔
792
                set_optional_defaults.append(set_default_assignment)
1✔
793

794
            if type_info.category == "array" and any(
1✔
795
                (dim != ":") and (not _is_int_literal(dim)) for dim in type_info.dimensions
796
            ):
797
                required_default_elements: int | None = None
1✔
798
                if has_default and default_values is not None:
1✔
799
                    if default_from_items:
1✔
800
                        required_default_elements = 1
1✔
801
                    elif array_default_spec is not None:
1✔
802
                        required_default_elements = len(array_default_spec.source_values)
1✔
803
                    else:
UNCOV
804
                        required_default_elements = len(default_values)
×
805
                if required_default_elements is not None and required_default_elements > 1:
1✔
806
                    runtime_default_extent_requirements.append(
1✔
807
                        {
808
                            "field": display_name,
809
                            "dimensions": list(type_info.dimensions),
810
                            "required_elements": required_default_elements,
811
                        }
812
                    )
813

814
            enum_values = _enum_values(prop, type_info, constants)
1✔
815
            if enum_values is not None:
1✔
816
                enum_category = _enum_category(type_info)
1✔
817
                enum_const_name = f"{name}_enum_values"
1✔
818
                enum_literals = [
1✔
819
                    _format_scalar_default(value, type_info.kind, enum_category)
820
                    for value in enum_values
821
                ]
822
                if enum_category == "string":
1✔
823
                    enum_array_literal = (
1✔
824
                        f"[{type_spec_with_defaults} :: {', '.join(enum_literals)}]"
825
                    )
826
                else:
827
                    enum_array_literal = f"[{', '.join(enum_literals)}]"
1✔
828
                if enum_category == "string":
1✔
829
                    enum_parameters.append(
1✔
830
                        f"{type_spec_with_defaults}, parameter, public :: &\n"
831
                        f"    {enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
832
                    )
833
                else:
834
                    enum_parameters.append(
1✔
835
                        f"{type_spec_with_defaults}, parameter, public :: "
836
                        f"{enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
837
                    )
838
                enum_type_info = (
1✔
839
                    _element_type_info(type_info) if type_info.category == "array" else type_info
840
                )
841
                _, missing_condition, _ = _sentinel_expressions(
1✔
842
                    enum_type_info,
843
                    var_ref="val",
844
                    len_ref="val",
845
                )
846
                enum_functions.append(
1✔
847
                    {
848
                        "name": name,
849
                        "func_name": f"{name}_in_enum",
850
                        "arg_type_spec": _enum_arg_type_spec(type_info),
851
                        "enum_values_name": enum_const_name,
852
                        "use_trim": enum_category == "string",
853
                        "missing_condition": missing_condition,
854
                    }
855
                )
856
                if type_info.category == "array":
1✔
857
                    enum_checks.append(
1✔
858
                        {
859
                            "name": name,
860
                            "display_name": display_name,
861
                            "func_name": f"{name}_in_enum",
862
                            "is_array": True,
863
                            "runtime_array": dynamic_array,
864
                            "array_ref": f"this%{name}",
865
                        }
866
                    )
867
                else:
868
                    enum_checks.append(
1✔
869
                        {
870
                            "name": name,
871
                            "display_name": display_name,
872
                            "func_name": f"{name}_in_enum",
873
                            "is_array": False,
874
                            "element_ref": f"this%{name}",
875
                        }
876
                    )
877

878
            bounds_spec = _bounds_spec(prop, type_info)
1✔
879
            if bounds_spec is not None:
1✔
880
                bounds_category = bounds_spec["category"]
1✔
881
                bounds_type_info = (
1✔
882
                    _element_type_info(type_info) if type_info.category == "array" else type_info
883
                )
884
                min_value = bounds_spec["min_value"]
1✔
885
                max_value = bounds_spec["max_value"]
1✔
886
                min_exclusive = bounds_spec["min_exclusive"]
1✔
887
                max_exclusive = bounds_spec["max_exclusive"]
1✔
888
                min_name = None
1✔
889
                max_name = None
1✔
890
                if min_value is not None:
1✔
891
                    min_name = f"{name}_min_excl" if min_exclusive else f"{name}_min"
1✔
892
                    min_literal = _format_scalar_default(
1✔
893
                        min_value, bounds_type_info.kind, bounds_category
894
                    )
895
                    bounds_parameters.append(
1✔
896
                        f"{bounds_type_info.type_spec}, parameter, public :: "
897
                        f"{min_name} = {min_literal}"
898
                    )
899
                if max_value is not None:
1✔
900
                    max_name = f"{name}_max_excl" if max_exclusive else f"{name}_max"
1✔
901
                    max_literal = _format_scalar_default(
1✔
902
                        max_value, bounds_type_info.kind, bounds_category
903
                    )
904
                    bounds_parameters.append(
1✔
905
                        f"{bounds_type_info.type_spec}, parameter, public :: "
906
                        f"{max_name} = {max_literal}"
907
                    )
908
                _, missing_condition, uses_ieee = _sentinel_expressions(
1✔
909
                    bounds_type_info,
910
                    var_ref="val",
911
                    len_ref="val",
912
                )
913
                if uses_ieee:
1✔
914
                    requires_ieee = True
1✔
915
                bounds_functions.append(
1✔
916
                    {
917
                        "name": name,
918
                        "func_name": f"{name}_in_bounds",
919
                        "arg_type_spec": bounds_type_info.arg_type_spec,
920
                        "has_min": min_value is not None,
921
                        "has_max": max_value is not None,
922
                        "min_name": min_name,
923
                        "max_name": max_name,
924
                        "min_exclusive": min_exclusive,
925
                        "max_exclusive": max_exclusive,
926
                        "missing_condition": missing_condition,
927
                    }
928
                )
929
                if type_info.category == "array":
1✔
930
                    bounds_checks.append(
1✔
931
                        {
932
                            "name": name,
933
                            "display_name": display_name,
934
                            "func_name": f"{name}_in_bounds",
935
                            "is_array": True,
936
                            "runtime_array": dynamic_array,
937
                            "array_ref": f"this%{name}",
938
                        }
939
                    )
940
                else:
941
                    bounds_checks.append(
1✔
942
                        {
943
                            "name": name,
944
                            "display_name": display_name,
945
                            "func_name": f"{name}_in_bounds",
946
                            "is_array": False,
947
                            "element_ref": f"this%{name}",
948
                        }
949
                    )
950

951
            is_array = type_info.category == "array"
1✔
952
            if is_required:
1✔
953
                if is_array:
1✔
954
                    # Match namelist-buffer semantics: set assigns the provided
955
                    # leading subsection and leaves completeness checks to is_valid.
956
                    set_required_assignments.append(
1✔
957
                        _render_partial_set_block(
958
                            name,
959
                            len(type_info.dimensions),
960
                            partial_bounds or [],
961
                        )
962
                    )
963
                else:
964
                    set_required_assignments.append(f"this%{name} = {name}")
1✔
965

966
            if not is_required:
1✔
967
                if is_array:
1✔
968
                    # Match namelist-buffer semantics: set assigns the provided
969
                    # leading subsection and leaves completeness checks to is_valid.
970
                    block = _render_partial_set_block(
1✔
971
                        name,
972
                        len(type_info.dimensions),
973
                        partial_bounds or [],
974
                    )
975
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
1✔
976
                    set_present_assignment = (
1✔
977
                        f"if (present({name})) then\n{indented_block}\nend if"
978
                    )
979
                else:
980
                    set_present_assignment = f"if (present({name})) this%{name} = {name}"
1✔
981
            else:
982
                set_present_assignment = None
1✔
983

984
            array_rank = len(type_info.dimensions) if is_array else 0
1✔
985
            element_condition: str | None = None
1✔
986

987
            if is_array and not has_default:
1✔
988
                element_type = _element_type_info(type_info)
1✔
989
                index_args = ", ".join(f"idx({idx})" for idx in range(1, array_rank + 1))
1✔
990
                element_ref = f"this%{name}({index_args})"
1✔
991
                _, element_condition, element_uses_ieee = _sentinel_expressions(
1✔
992
                    element_type,
993
                    var_ref=element_ref,
994
                    len_ref=f"this%{name}",
995
                )
996
                if element_uses_ieee:
1✔
997
                    requires_ieee = True
1✔
998

999
            if has_default or type_info.category == "boolean":
1✔
1000
                presence_cases.append(
1✔
1001
                    {
1002
                        "name": name,
1003
                        "display_name": display_name,
1004
                        "always_true": True,
1005
                        "sentinel_condition": None,
1006
                        "is_array": is_array,
1007
                        "runtime_array": dynamic_array,
1008
                        "rank": array_rank,
1009
                        "element_condition": element_condition,
1010
                    }
1011
                )
1012
            else:
1013
                if set_sentinel_condition is None:
1✔
UNCOV
1014
                    raise ValueError(f"missing sentinel condition for '{display_name}'")
×
1015
                presence_cases.append(
1✔
1016
                    {
1017
                        "name": name,
1018
                        "display_name": display_name,
1019
                        "always_true": False,
1020
                        "sentinel_condition": set_sentinel_condition,
1021
                        "is_array": is_array,
1022
                        "runtime_array": dynamic_array,
1023
                        "rank": array_rank,
1024
                        "element_condition": element_condition,
1025
                    }
1026
                )
1027

1028
            fields.append(
1✔
1029
                FieldSpec(
1030
                    order=index,
1031
                    name=name,
1032
                    title=title,
1033
                    description=description,
1034
                    declaration=declaration_with_doc,
1035
                    local_declaration=local_decl,
1036
                    required=is_required,
1037
                    sentinel_assignment=sentinel_assignment,
1038
                    sentinel_check=(
1039
                        f"if ({sentinel_condition}) error stop "
1040
                        f"\"{module_name}%from_file: '{name}' is required\""
1041
                        if sentinel_condition and is_required
1042
                        else None
1043
                    ),
1044
                    default_assignment=default_assignment,
1045
                    set_default_assignment=set_default_assignment,
1046
                    set_present_assignment=set_present_assignment,
1047
                    argument_declaration=argument_decl,
1048
                    type_category=type_info.category,
1049
                    runtime_sized_array=dynamic_array,
1050
                    rank=len(type_info.dimensions),
1051
                )
1052
            )
1053

1054
    except ValueError as exc:
1✔
1055
        if current_property is None:
1✔
UNCOV
1056
            raise
×
1057
        msg = str(exc)
1✔
1058
        if f"property '{current_property}'" in msg:
1✔
UNCOV
1059
            raise
×
1060
        raise ValueError(f"property '{current_property}': {msg}") from exc
1✔
1061
    if uses_partly_set and "NML_ERR_PARTLY_SET" not in helper_imports:
1✔
1062
        helper_imports.append("NML_ERR_PARTLY_SET")
1✔
1063
    required_flex_names = {entry["name"] for entry in flex_arrays if entry["required"]}
1✔
1064
    required_scalar_validations: list[str] = []
1✔
1065
    for name in required_fields:
1✔
1066
        if name in required_scalar_names:
1✔
1067
            required_scalar_validations.append(property_name_map[name])
1✔
1068
        elif name not in required_array_by_name and name not in required_flex_names:
1✔
UNCOV
1069
            required_scalar_validations.append(property_name_map[name])
×
1070
    required_array_validations = [
1✔
1071
        required_array_by_name[name]
1072
        for name in required_fields
1073
        if name in required_array_by_name
1074
    ]
1075
    namelist_vars = [field.name for field in fields]
1✔
1076
    required_fields_specs = [field for field in fields if field.required]
1✔
1077
    optional_fields_specs = [field for field in fields if not field.required]
1✔
1078

1079
    resolved_kind_module = kind_module or "iso_fortran_env"
1✔
1080
    if not isinstance(resolved_kind_module, str) or not resolved_kind_module:
1✔
1081
        raise ValueError("kind module must be a non-empty string")
×
1082

1083
    set_dims_arguments: list[dict[str, Any]] = []
1✔
1084
    candidate_names_in_use: set[str] = set()
1✔
1085
    for entry in runtime_dimensions:
1✔
1086
        candidate_base = f"candidate_{entry['name']}"
1✔
1087
        candidate_name = _unique_generated_name(candidate_base, candidate_names_in_use)
1✔
1088
        candidate_names_in_use.add(candidate_name.lower())
1✔
1089
        set_dims_arguments.append(
1✔
1090
            {
1091
                "name": entry["name"],
1092
                "default_name": entry["default_name"],
1093
                "local_name": entry["local_name"],
1094
                "arg_name": entry["name"],
1095
                "candidate_name": candidate_name,
1096
                "min_required": 1,
1097
            }
1098
        )
1099

1100
    candidate_name_map: dict[str, str] = {
1✔
1101
        str(entry["name"]): str(entry["candidate_name"]) for entry in set_dims_arguments
1102
    }
1103
    set_dims_extent_checks: list[dict[str, str]] = []
1✔
1104
    seen_extent_checks: set[tuple[str, str]] = set()
1✔
1105
    for requirement in runtime_default_extent_requirements:
1✔
1106
        factors: list[str] = []
1✔
1107
        requirement_dimensions = cast("list[str]", requirement["dimensions"])
1✔
1108
        for extent_dim in requirement_dimensions:
1✔
1109
            if extent_dim == ":":
1✔
1110
                continue
×
1111
            if _is_int_literal(extent_dim):
1✔
1112
                factors.append(extent_dim)
1✔
1113
            else:
1114
                factors.append(candidate_name_map.get(extent_dim.lower(), extent_dim))
1✔
1115
        if not factors:
1✔
UNCOV
1116
            continue
×
1117
        product_expr = " * ".join(factors)
1✔
1118
        if len(factors) > 1:
1✔
1119
            product_expr = f"({product_expr})"
1✔
1120
        required_elements = cast("int", requirement["required_elements"])
1✔
1121
        condition = f"{product_expr} < {required_elements}"
1✔
1122
        field_name = cast("str", requirement["field"])
1✔
1123
        extent_message = (
1✔
1124
            f"shape constants for '{field_name}' must allow at least "
1125
            f"{required_elements} default values"
1126
        )
1127
        extent_key = (condition, extent_message)
1✔
1128
        if extent_key in seen_extent_checks:
1✔
UNCOV
1129
            continue
×
1130
        seen_extent_checks.add(extent_key)
1✔
1131
        set_dims_extent_checks.append({"condition": condition, "message": extent_message})
1✔
1132

1133
    context = {
1✔
1134
        "module_name": module_name,
1135
        "type_name": type_name,
1136
        "type_prefix": module_name,
1137
        "doc_class": doc_class,
1138
        "brief_text": brief_text,
1139
        "details_text": details_text,
1140
        "module_doc": module_doc,
1141
        "namelist_name": namelist_name,
1142
        "fields": fields,
1143
        "runtime_dimensions": runtime_dimensions,
1144
        "runtime_allocations": runtime_allocations,
1145
        "runtime_deallocations": runtime_deallocations,
1146
        "runtime_local_allocations": runtime_local_allocations,
1147
        "namelist_vars": namelist_vars,
1148
        "sentinel_assignments": sentinel_assignments,
1149
        "default_assignments": default_assignments,
1150
        "default_parameters": default_parameters,
1151
        "enum_parameters": enum_parameters,
1152
        "bounds_parameters": bounds_parameters,
1153
        "local_init_assignments": local_init_assignments,
1154
        "required_scalar_validations": required_scalar_validations,
1155
        "required_array_validations": required_array_validations,
1156
        "flex_arrays": flex_arrays,
1157
        "assignments": [f"this%{field.name} = {field.name}" for field in fields],
1158
        "argument_list": [field.name for field in required_fields_specs + optional_fields_specs],
1159
        "required_argument_declarations": [
1160
            field.argument_declaration for field in required_fields_specs
1161
        ],
1162
        "optional_argument_declarations": [
1163
            field.argument_declaration for field in optional_fields_specs
1164
        ],
1165
        "set_dims_arguments": set_dims_arguments,
1166
        "set_dims_extent_checks": set_dims_extent_checks,
1167
        "set_required_assignments": set_required_assignments,
1168
        "set_optional_defaults": set_optional_defaults,
1169
        "set_optional_present": [
1170
            field.set_present_assignment
1171
            for field in optional_fields_specs
1172
            if field.set_present_assignment
1173
        ],
1174
        "enum_functions": enum_functions,
1175
        "enum_checks": enum_checks,
1176
        "bounds_functions": bounds_functions,
1177
        "bounds_checks": bounds_checks,
1178
        "kind_module": resolved_kind_module,
1179
        "kind_imports": _resolve_kind_imports(
1180
            kind_ids,
1181
            kind_map=kind_map,
1182
            kind_allowlist=kind_allowlist,
1183
        ),
1184
        "use_ieee": requires_ieee,
1185
        "helper_module": helper_module,
1186
        "helper_imports": helper_imports,
1187
        "presence_cases": presence_cases,
1188
        "flex_bound_vars": _sort_bound_vars(flex_bound_vars),
1189
        "f2py_handle_helpers": f2py_handle_helpers,
1190
    }
1191

1192
    return context
1✔
1193

1194

1195
def _ordered_unique(values: Iterable[Any]) -> list[Any]:
1✔
1196
    seen: set[Any] = set()
1✔
1197
    ordered: list[Any] = []
1✔
1198
    for value in values:
1✔
1199
        if value not in seen:
1✔
1200
            seen.add(value)
1✔
1201
            ordered.append(value)
1✔
1202
    return ordered
1✔
1203

1204

1205
def _reject_runtime_dimension_lengths(
1✔
1206
    prop: dict[str, Any],
1207
    dimensions: dict[str, int],
1208
) -> None:
1209
    if not dimensions:
1✔
1210
        return
1✔
1211
    if prop.get("type") == "string":
1✔
1212
        length = prop.get("x-fortran-len")
1✔
1213
        if isinstance(length, str) and length.strip().lower() in dimensions:
1✔
1214
            raise ValueError(f"dimension '{length.strip()}' cannot be used as x-fortran-len")
1✔
1215
    if prop.get("type") == "array":
1✔
1216
        items = prop.get("items")
1✔
1217
        if isinstance(items, dict):
1✔
1218
            _reject_runtime_dimension_lengths(items, dimensions)
1✔
1219

1220

1221
def _resolve_kind_imports(
1✔
1222
    kind_ids: list[str],
1223
    *,
1224
    kind_map: dict[str, str] | None,
1225
    kind_allowlist: Iterable[str] | None,
1226
) -> list[str]:
1227
    allowlist = set(kind_allowlist) if kind_allowlist is not None else None
1✔
1228
    imports: list[str] = []
1✔
1229
    target_aliases: dict[str, str] = {}
1✔
1230
    for kind_id in _ordered_unique(kind_ids):
1✔
1231
        target = kind_id
1✔
1232
        if kind_map is not None and kind_id in kind_map:
1✔
1233
            mapped = kind_map[kind_id]
1✔
1234
            if not isinstance(mapped, str):
1✔
UNCOV
1235
                raise ValueError(f"kind map target for '{kind_id}' must be a string")
×
1236
            target = mapped
1✔
1237
        elif allowlist is not None and kind_id not in allowlist:
1✔
UNCOV
1238
            raise ValueError(f"kind '{kind_id}' not present in kind map or kind module list")
×
1239

1240
        if allowlist is not None and target not in allowlist:
1✔
UNCOV
1241
            raise ValueError(
×
1242
                f"kind map target '{target}' for '{kind_id}' not present in kind module list"
1243
            )
1244

1245
        if target != kind_id:
1✔
1246
            existing = target_aliases.get(target)
1✔
1247
            if existing is not None and existing != kind_id:
1✔
UNCOV
1248
                raise ValueError(
×
1249
                    f"kind map target '{target}' is shared by '{existing}' and '{kind_id}'"
1250
                )
1251
            target_aliases[target] = kind_id
1✔
1252
            imports.append(f"{kind_id}=>{target}")
1✔
1253
        else:
1254
            imports.append(kind_id)
1✔
1255
    return imports
1✔
1256

1257

1258
def _field_type_info(
1✔
1259
    prop: dict[str, Any],
1260
    constants: dict[str, int] | None,
1261
) -> FieldTypeInfo:
1262
    prop_type = prop.get("type")
1✔
1263
    if prop_type == "array":
1✔
1264
        dimensions: list[str] = []
1✔
1265
        current = prop
1✔
1266
        while current.get("type") == "array":
1✔
1267
            dimensions.extend(_extract_dimensions(current))
1✔
1268
            items = current.get("items")
1✔
1269
            if not isinstance(items, dict):
1✔
UNCOV
1270
                raise ValueError("array property must define 'items'")
×
1271
            if items.get("type") == "array":
1✔
1272
                raise ValueError("nested array properties are not supported; use x-fortran-shape")
1✔
1273
            current = items
1✔
1274
        scalar = _scalar_type_info(current, constants)
1✔
1275
        return FieldTypeInfo(
1✔
1276
            type_spec=scalar.type_spec,
1277
            arg_type_spec=scalar.arg_type_spec,
1278
            dimensions=dimensions,
1279
            kind=scalar.kind,
1280
            category="array",
1281
            length_expr=scalar.length_expr,
1282
            element_category=scalar.category,
1283
        )
1284

1285
    scalar = _scalar_type_info(prop, constants)
1✔
1286
    return FieldTypeInfo(
1✔
1287
        type_spec=scalar.type_spec,
1288
        arg_type_spec=scalar.arg_type_spec,
1289
        dimensions=[],
1290
        kind=scalar.kind,
1291
        category=scalar.category,
1292
        length_expr=scalar.length_expr,
1293
        element_category=None,
1294
    )
1295

1296

1297
def _element_type_info(type_info: FieldTypeInfo) -> FieldTypeInfo:
1✔
1298
    if type_info.category != "array":
1✔
UNCOV
1299
        raise ValueError("element type info requires an array field")
×
1300
    element_category = type_info.element_category
1✔
1301
    if element_category is None:
1✔
UNCOV
1302
        raise ValueError("array field missing element category")
×
1303
    return FieldTypeInfo(
1✔
1304
        type_spec=type_info.type_spec,
1305
        arg_type_spec=type_info.type_spec,
1306
        dimensions=[],
1307
        kind=type_info.kind,
1308
        category=element_category,
1309
        length_expr=type_info.length_expr,
1310
        element_category=None,
1311
    )
1312

1313

1314
def _enum_category(type_info: FieldTypeInfo) -> str:
1✔
1315
    if type_info.category == "array":
1✔
1316
        if type_info.element_category is None:
1✔
UNCOV
1317
            raise ValueError("array field missing element category")
×
1318
        return type_info.element_category
1✔
1319
    return type_info.category
1✔
1320

1321

1322
def _enum_arg_type_spec(type_info: FieldTypeInfo) -> str:
1✔
1323
    category = _enum_category(type_info)
1✔
1324
    if category == "string":
1✔
1325
        return "character(len=*)"
1✔
1326
    return type_info.type_spec
1✔
1327

1328

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

1413

1414
def _extract_dimensions(prop: dict[str, Any]) -> list[str]:
1✔
1415
    shape = prop.get("x-fortran-shape")
1✔
1416
    if isinstance(shape, bool):
1✔
UNCOV
1417
        raise ValueError("array property 'x-fortran-shape' must not be a boolean")
×
1418
    if isinstance(shape, int):
1✔
1419
        return [str(shape)]
1✔
1420
    if isinstance(shape, str):
1✔
1421
        dim = shape.strip()
1✔
1422
        if not dim:
1✔
1423
            raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
1424
        _validate_dimension_token(dim)
1✔
1425
        return [dim]
1✔
1426
    if isinstance(shape, list):
1✔
1427
        dimensions: list[str] = []
1✔
1428
        for dim in shape:
1✔
1429
            if isinstance(dim, bool):
1✔
UNCOV
1430
                raise ValueError("array property 'x-fortran-shape' must not include booleans")
×
1431
            if isinstance(dim, int):
1✔
1432
                dim_literal = str(dim)
1✔
1433
            elif isinstance(dim, str):
1✔
1434
                dim_literal = dim.strip()
1✔
1435
                if not dim_literal:
1✔
UNCOV
1436
                    raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
1437
            else:
UNCOV
1438
                raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
1439
            _validate_dimension_token(dim_literal)
1✔
1440
            dimensions.append(dim_literal)
1✔
1441
        return dimensions
1✔
1442
    if shape is None:
1✔
1443
        raise ValueError("array property must define 'x-fortran-shape'")
1✔
UNCOV
1444
    raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
1445

1446

1447
def _is_int_literal(value: str) -> bool:
1✔
1448
    try:
1✔
1449
        int(value)
1✔
1450
    except ValueError:
1✔
1451
        return False
1✔
1452
    return True
1✔
1453

1454

1455
def _validate_dimension_token(dim: str) -> None:
1✔
1456
    if dim == ":":
1✔
UNCOV
1457
        return
×
1458
    if _is_int_literal(dim):
1✔
1459
        return
1✔
1460
    if FORTRAN_IDENTIFIER.match(dim):
1✔
1461
        return
1✔
UNCOV
1462
    raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
1463

1464

1465
def _validate_length_token(length_expr: str) -> None:
1✔
1466
    if _is_int_literal(length_expr):
1✔
UNCOV
1467
        return
×
1468
    if FORTRAN_IDENTIFIER.match(length_expr):
1✔
1469
        return
1✔
UNCOV
1470
    raise ValueError("string length must be an integer literal or identifier")
×
1471

1472

1473
def _parse_flex_dim(prop: dict[str, Any], type_info: FieldTypeInfo) -> int:
1✔
1474
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
1475
    if flex_raw is None:
1✔
1476
        flex_value = 0
1✔
1477
    else:
1478
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
1✔
1479
            raise ValueError("x-fortran-flex-tail-dims must be an integer")
×
1480
        flex_value = flex_raw
1✔
1481
    if flex_value < 0:
1✔
1482
        raise ValueError("x-fortran-flex-tail-dims must be >= 0")
×
1483
    if flex_value == 0:
1✔
1484
        return 0
1✔
1485
    if type_info.category != "array":
1✔
1486
        raise ValueError("x-fortran-flex-tail-dims is only supported for arrays")
1✔
1487
    if flex_value > len(type_info.dimensions):
1✔
1488
        raise ValueError("x-fortran-flex-tail-dims must not exceed array rank")
1✔
1489
    if any(dim == ":" for dim in type_info.dimensions):
1✔
UNCOV
1490
        raise ValueError(
×
1491
            "x-fortran-flex-tail-dims does not support deferred-size dimensions"
1492
        )
1493
    return flex_value
1✔
1494

1495

1496
def _collect_dimension_constants(
1✔
1497
    dimensions: list[str],
1498
    constants: dict[str, int] | None,
1499
) -> list[str]:
1500
    used: list[str] = []
1✔
1501
    for dim in dimensions:
1✔
1502
        if dim == ":" or _is_int_literal(dim):
1✔
1503
            continue
1✔
1504
        if not FORTRAN_IDENTIFIER.match(dim):
1✔
UNCOV
1505
            raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
1506
        dim_key = dim.lower()
1✔
1507
        if constants is None or dim_key not in constants:
1✔
1508
            raise ValueError(f"dimension constant '{dim}' is not defined in config")
1✔
1509
        value = constants[dim_key]
1✔
1510
        if isinstance(value, bool) or not isinstance(value, int):
1✔
UNCOV
1511
            raise ValueError(f"dimension constant '{dim}' must be an integer")
×
1512
        if dim not in used:
1✔
1513
            used.append(dim)
1✔
1514
    return used
1✔
1515

1516

1517
def _render_declaration(type_spec: str, dimensions: list[str], name: str) -> str:
1✔
1518
    parts = [type_spec]
1✔
1519
    if dimensions:
1✔
1520
        dims = ", ".join(dimensions)
1✔
1521
        parts.append(f"dimension({dims})")
1✔
1522
    return f"{', '.join(parts)} :: {name}"
1✔
1523

1524

1525
def _render_runtime_declaration(
1✔
1526
    type_info: FieldTypeInfo,
1527
    name: str,
1528
) -> str:
1529
    if type_info.category == "string":
1✔
UNCOV
1530
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
1531
    if type_info.category != "array":
1✔
UNCOV
1532
        raise ValueError(
×
1533
            "runtime declaration is only supported for arrays, "
1534
            f"got category '{type_info.category}'"
1535
        )
1536

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

1540

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

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

1559

1560
def _render_runtime_allocations(
1✔
1561
    type_info: FieldTypeInfo,
1562
    name: str,
1563
    *,
1564
    runtime_dimensions: list[str],
1565
    runtime_length_expr: str | None,
1566
    target_prefix: str = "",
1567
) -> list[str]:
1568
    lines: list[str] = []
1✔
1569
    target_ref = f"{target_prefix}{name}"
1✔
1570
    if type_info.category != "array":
1✔
UNCOV
1571
        raise ValueError("runtime allocation is only supported for arrays")
×
1572
    if any(dim == ":" for dim in runtime_dimensions):
1✔
UNCOV
1573
        raise ValueError("runtime-sized arrays do not support deferred-size dimensions")
×
1574

1575
    lines.append(f"if (allocated({target_ref})) deallocate({target_ref})")
1✔
1576
    dims_expr = ", ".join(runtime_dimensions)
1✔
1577
    if type_info.element_category == "string" and runtime_length_expr is not None:
1✔
1578
        lines.append(f"allocate(character(len={runtime_length_expr}) :: {target_ref}({dims_expr}))")
1✔
1579
    else:
1580
        lines.append(f"allocate({target_ref}({dims_expr}))")
1✔
1581
    return lines
1✔
1582

1583

1584
def _render_runtime_deallocations(name: str, *, target_prefix: str = "") -> list[str]:
1✔
1585
    target_ref = f"{target_prefix}{name}"
1✔
1586
    return [f"if (allocated({target_ref})) deallocate({target_ref})"]
1✔
1587

1588

1589
def _render_runtime_local_allocations(
1✔
1590
    type_info: FieldTypeInfo,
1591
    name: str,
1592
    *,
1593
    runtime_dimensions: list[str],
1594
    runtime_length_expr: str | None,
1595
) -> list[str]:
1596
    return _render_runtime_allocations(
1✔
1597
        type_info,
1598
        name,
1599
        runtime_dimensions=runtime_dimensions,
1600
        runtime_length_expr=runtime_length_expr,
1601
    )
1602

1603

1604
def _array_section_ref(base_ref: str, rank: int) -> str:
1✔
1605
    dims = ", ".join(":" for _ in range(rank))
1✔
1606
    return f"{base_ref}({dims})"
1✔
1607

1608

1609
def _render_argument_declaration(
1✔
1610
    *,
1611
    name: str,
1612
    type_info: FieldTypeInfo,
1613
    is_required: bool,
1614
    dimensions: list[str] | None = None,
1615
    doc: str | None = None,
1616
) -> str:
1617
    intent = "intent(in)"
1✔
1618
    parts = [type_info.arg_type_spec]
1✔
1619
    arg_dimensions = dimensions if dimensions is not None else type_info.dimensions
1✔
1620
    if arg_dimensions:
1✔
1621
        dims = ", ".join(arg_dimensions)
1✔
1622
        parts.append(f"dimension({dims})")
1✔
1623
    if not is_required:
1✔
1624
        parts.append(intent)
1✔
1625
        parts.append("optional")
1✔
1626
        decl = f"{', '.join(parts[:-1])}, {parts[-1]} :: {name}"
1✔
1627
    else:
1628
        parts.append(intent)
1✔
1629
        decl = f"{', '.join(parts)} :: {name}"
1✔
1630
    if doc:
1✔
1631
        decl = f"{decl} !< {doc}"
1✔
1632
    return decl
1✔
1633

1634

1635
def _sentinel_comment(type_info: FieldTypeInfo, *, required: bool) -> str:
1✔
1636
    label = "required" if required else "optional"
1✔
1637
    category = type_info.category
1✔
1638
    if category == "array":
1✔
1639
        element = type_info.element_category or "array"
1✔
1640
        return f" ! sentinel for {label} {element} array"
1✔
1641
    if category == "string":
1✔
1642
        if required:
1✔
1643
            return " ! NULL string as sentinel for required string"
1✔
1644
        return " ! sentinel for optional string"
1✔
1645
    if category == "integer":
1✔
1646
        return f" ! sentinel for {label} integer"
1✔
1647
    if category == "real":
1✔
1648
        return f" ! sentinel for {label} real"
1✔
UNCOV
1649
    return ""
×
1650

1651

1652
def _render_sentinel_assignment(
1✔
1653
    type_info: FieldTypeInfo,
1654
    *,
1655
    target_ref: str,
1656
    value_expr: str,
1657
    comment: str,
1658
) -> str:
1659
    if type_info.category == "string":
1✔
1660
        return _render_string_sentinel_assignment(
1✔
1661
            target_ref=target_ref,
1662
            value_expr=value_expr,
1663
            comment=comment,
1664
        )
1665
    if type_info.category == "array" and type_info.element_category == "string":
1✔
1666
        return _render_string_array_sentinel_assignment(
1✔
1667
            target_ref=target_ref,
1668
            value_expr=value_expr,
1669
            comment=comment,
1670
        )
1671
    return f"{target_ref} = {value_expr}{comment}"
1✔
1672

1673

1674
def _render_string_sentinel_assignment(
1✔
1675
    *,
1676
    target_ref: str,
1677
    value_expr: str,
1678
    comment: str,
1679
) -> str:
1680
    return f"{target_ref} = {value_expr}{comment}"
1✔
1681

1682

1683
def _render_string_array_sentinel_assignment(
1✔
1684
    *,
1685
    target_ref: str,
1686
    value_expr: str,
1687
    comment: str,
1688
) -> str:
1689
    return f"{target_ref} = {value_expr}{comment}"
1✔
1690

1691

1692
def _sentinel_expressions(
1✔
1693
    type_info: FieldTypeInfo,
1694
    *,
1695
    var_ref: str,
1696
    len_ref: str | None = None,
1697
) -> tuple[str, str, bool]:
1698
    category = type_info.category
1✔
1699
    if category == "array":
1✔
1700
        element = type_info.element_category
1✔
1701
        if element == "string":
1✔
1702
            return "achar(0)", f"all({var_ref} == achar(0))", False
1✔
1703
        if element == "integer":
1✔
1704
            return f"-huge({var_ref})", f"all({var_ref} == -huge({var_ref}))", False
1✔
1705
        if element == "real":
1✔
1706
            return (
1✔
1707
                f"ieee_value({var_ref}, ieee_quiet_nan)",
1708
                f"all(ieee_is_nan({var_ref}))",
1709
                True,
1710
            )
UNCOV
1711
        if element == "boolean":
×
UNCOV
1712
            raise ValueError("boolean arrays cannot use sentinels")
×
UNCOV
1713
        raise ValueError(f"unsupported sentinel array element '{element}'")
×
1714
    if category == "string":
1✔
1715
        return "achar(0)", f"{var_ref} == achar(0)", False
1✔
1716
    if category == "integer":
1✔
1717
        return f"-huge({var_ref})", f"{var_ref} == -huge({var_ref})", False
1✔
1718
    if category == "real":
1✔
1719
        return (
1✔
1720
            f"ieee_value({var_ref}, ieee_quiet_nan)",
1721
            f"ieee_is_nan({var_ref})",
1722
            True,
1723
        )
1724
    if category == "boolean":
×
1725
        raise ValueError("boolean values cannot use sentinels")
×
UNCOV
1726
    raise ValueError(f"unsupported sentinel category '{category}'")
×
1727

1728

1729
def _element_missing_expression(
1✔
1730
    category: str,
1731
    *,
1732
    var_ref: str,
1733
    len_ref: str | None = None,
1734
) -> tuple[str, bool]:
1735
    if category == "string":
1✔
1736
        return f"{var_ref} == achar(0)", False
×
1737
    if category == "integer":
1✔
1738
        return f"{var_ref} == -huge({var_ref})", False
1✔
1739
    if category == "real":
1✔
1740
        return f"ieee_is_nan({var_ref})", True
1✔
UNCOV
1741
    raise ValueError(f"unsupported missing category '{category}'")
×
1742

1743

1744
def _array_missing_conditions(
1✔
1745
    element_category: str,
1746
    *,
1747
    var_ref: str,
1748
    len_ref: str | None = None,
1749
) -> tuple[str, str, bool]:
1750
    missing_expr, uses_ieee = _element_missing_expression(
1✔
1751
        element_category, var_ref=var_ref, len_ref=len_ref
1752
    )
1753
    return f"all({missing_expr})", f"any({missing_expr})", uses_ieee
1✔
1754

1755

1756
def _slice_ref(name: str, rank: int, dim: int, index_var: str) -> str:
1✔
1757
    dims = [":" for _ in range(rank)]
1✔
1758
    dims[dim - 1] = index_var
1✔
1759
    return f"this%{name}({', '.join(dims)})"
1✔
1760

1761

1762
def _flex_bound_vars(dim: int) -> tuple[str, str]:
1✔
1763
    return f"lb_{dim}", f"ub_{dim}"
1✔
1764

1765

1766
def _slice_ref_bounds(
1✔
1767
    name: str,
1768
    rank: int,
1769
    flex_dims: list[int],
1770
    lb_vars: dict[int, str],
1771
    ub_vars: dict[int, str],
1772
) -> str:
1773
    dims: list[str] = []
1✔
1774
    for dim in range(1, rank + 1):
1✔
1775
        if dim in flex_dims:
1✔
1776
            dims.append(f"{lb_vars[dim]}:{ub_vars[dim]}")
1✔
1777
        else:
1778
            dims.append(":")
1✔
1779
    return f"this%{name}({', '.join(dims)})"
1✔
1780

1781

1782
def _render_partial_set_block(
1✔
1783
    name: str,
1784
    rank: int,
1785
    bounds: list[dict[str, Any]],
1786
) -> str:
1787
    lb_vars = {entry["dim"]: entry["lb_var"] for entry in bounds}
1✔
1788
    ub_vars = {entry["dim"]: entry["ub_var"] for entry in bounds}
1✔
1789
    dims_all = [entry["dim"] for entry in bounds]
1✔
1790
    lines: list[str] = []
1✔
1791
    for entry in bounds:
1✔
1792
        dim = entry["dim"]
1✔
1793
        lb_var = entry["lb_var"]
1✔
1794
        ub_var = entry["ub_var"]
1✔
1795
        lines.append(f"if (size({name}, {dim}) > size(this%{name}, {dim})) then")
1✔
1796
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
1797
        lines.append(
1✔
1798
            f"  if (present(errmsg)) errmsg = \"dimension {dim} exceeds bounds for '{name}'\""
1799
        )
1800
        lines.append("  return")
1✔
1801
        lines.append("end if")
1✔
1802
        lines.append(f"{lb_var} = lbound(this%{name}, {dim})")
1✔
1803
        lines.append(f"{ub_var} = {lb_var} + size({name}, {dim}) - 1")
1✔
1804
    target_ref = _slice_ref_bounds(name, rank, dims_all, lb_vars, ub_vars)
1✔
1805
    lines.append(f"{target_ref} = {name}")
1✔
1806
    return "\n".join(lines)
1✔
1807

1808

1809
def _sort_bound_vars(values: set[str]) -> list[str]:
1✔
1810
    def sort_key(name: str) -> tuple[str, int]:
1✔
1811
        prefix, _, suffix = name.partition("_")
1✔
1812
        try:
1✔
1813
            return prefix, int(suffix)
1✔
UNCOV
1814
        except ValueError:
×
UNCOV
1815
            return prefix, 0
×
1816

1817
    return sorted(values, key=sort_key)
1✔
1818

1819

1820
def _format_reshape_assignment(name: str, arguments: list[str]) -> str:
1✔
1821
    lines = [f"this%{name} = reshape( &"]
1✔
1822
    for index, arg in enumerate(arguments):
1✔
1823
        suffix = ", &" if index < len(arguments) - 1 else ")"
1✔
1824
        lines.append(f"  {arg}{suffix}")
1✔
1825
    return "\n".join(lines)
1✔
1826

1827

1828
def _format_default(
1✔
1829
    value: Any,
1830
    type_info: FieldTypeInfo,
1831
    prop: dict[str, Any],
1832
    constants: dict[str, int] | None = None,
1833
) -> str:
1834
    if type_info.category == "array":
1✔
UNCOV
1835
        if not isinstance(value, list):
×
UNCOV
1836
            raise ValueError("array default must be a list")
×
UNCOV
1837
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
UNCOV
1838
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
UNCOV
1839
        elements = [
×
1840
            _format_scalar_default(element, type_info.kind, type_info.element_category)
1841
            for element in array_default.source_values
1842
        ]
UNCOV
1843
        if (
×
1844
            len(type_info.dimensions) == 1
1845
            and array_default.order_values is None
1846
            and array_default.pad_values is None
1847
        ):
1848
            return f"[{', '.join(elements)}]"
×
1849

1850
        shape_literal = ", ".join(type_info.dimensions)
×
1851
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
1852

UNCOV
1853
        if array_default.order_values is not None:
×
UNCOV
1854
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
1855
            arguments.append(f"order=[{order_literal}]")
×
1856

UNCOV
1857
        if array_default.pad_values is not None:
×
UNCOV
1858
            pad_elements = [
×
1859
                _format_scalar_default(element, type_info.kind, type_info.element_category)
1860
                for element in array_default.pad_values
1861
            ]
1862
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
1863

UNCOV
1864
        return f"reshape({', '.join(arguments)})"
×
1865
    return _format_scalar_default(value, type_info.kind, type_info.category)
1✔
1866

1867

1868
def _format_scalar_default(value: Any, kind: str | None, category: str | None) -> str:
1✔
1869
    if category == "integer":
1✔
1870
        if not isinstance(value, int):
1✔
UNCOV
1871
            raise ValueError("integer default must be an int")
×
1872
        suffix = f"_{kind}" if kind else ""
1✔
1873
        return f"{value}{suffix}"
1✔
1874
    if category == "real":
1✔
1875
        number = float(value)
1✔
1876
        literal = repr(number)
1✔
1877
        if literal.lower() == "nan":
1✔
UNCOV
1878
            raise ValueError("NaN defaults are not supported")
×
1879
        if "E" in literal:
1✔
UNCOV
1880
            literal = literal.replace("E", "e")
×
1881
        if "." not in literal and "e" not in literal:
1✔
UNCOV
1882
            literal = f"{literal}.0"
×
1883
        suffix = f"_{kind}" if kind else ""
1✔
1884
        return f"{literal}{suffix}"
1✔
1885
    if category == "boolean":
1✔
1886
        if not isinstance(value, bool):
1✔
UNCOV
1887
            raise ValueError("boolean default must be a bool")
×
1888
        return ".true." if value else ".false."
1✔
1889
    if category == "string":
1✔
1890
        if not isinstance(value, str):
1✔
UNCOV
1891
            raise ValueError("string default must be a str")
×
1892
        escaped = value.replace('"', '""')
1✔
1893
        return f'"{escaped}"'
1✔
1894
    raise ValueError(f"unsupported default category '{category}'")
×
1895

1896

1897
def _parse_default_dimensions(
1✔
1898
    dimensions: list[str],
1899
    constants: dict[str, int] | None,
1900
) -> list[int]:
1901
    if not dimensions:
1✔
UNCOV
1902
        raise ValueError("array property missing dimensions")
×
1903
    parsed: list[int] = []
1✔
1904
    for dim in dimensions:
1✔
1905
        if dim == ":":
1✔
1906
            raise ValueError("defaults not supported for deferred-size dimensions")
×
1907
        try:
1✔
1908
            parsed.append(int(dim))
1✔
1909
        except (TypeError, ValueError) as err:  # pragma: no cover - defensive
1910
            dim_key = dim.lower()
1911
            if constants is None or dim_key not in constants:
1912
                raise ValueError(
1913
                    "array default dimensions must be integer literals or defined constants"
1914
                ) from err
1915
            value = constants[dim_key]
1916
            if isinstance(value, bool) or not isinstance(value, int):
1917
                raise ValueError("array default dimension constants must be integers") from err
1918
            parsed.append(value)
1919
    return parsed
1✔
1920

1921

1922
def _prepare_array_default(
1✔
1923
    value: list[Any],
1924
    dims: list[int],
1925
    prop: dict[str, Any],
1926
) -> ArrayDefaultSpec:
1927
    default_values = _ensure_flat_scalar_list(value, "array default")
1✔
1928
    if not default_values:
1✔
UNCOV
1929
        raise ValueError("array default must contain at least one value")
×
1930

1931
    total_size = math.prod(dims)
1✔
1932
    if len(default_values) > total_size:
1✔
UNCOV
1933
        raise ValueError("array default longer than declared x-fortran-shape")
×
1934

1935
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
1936
    if not isinstance(order_raw, str):
1✔
UNCOV
1937
        raise ValueError("array default order must be 'F' or 'C'")
×
1938
    order = order_raw.upper()
1✔
1939
    if order not in {"F", "C"}:
1✔
UNCOV
1940
        raise ValueError("array default order must be 'F' or 'C'")
×
1941

1942
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
1943
    if not isinstance(repeat_raw, bool):
1✔
UNCOV
1944
        raise ValueError("array default repeat must be a boolean")
×
1945
    repeat = bool(repeat_raw)
1✔
1946

1947
    pad_raw = prop.get("x-fortran-default-pad")
1✔
1948
    pad_values: list[Any] | None = None
1✔
1949
    if pad_raw is not None:
1✔
1950
        if repeat:
1✔
UNCOV
1951
            raise ValueError("array default cannot set both pad and repeat")
×
1952
        if not isinstance(pad_raw, list):
1✔
1953
            pad_raw = [pad_raw]
1✔
1954
        pad_values = _ensure_flat_scalar_list(pad_raw, "array default pad")
1✔
1955
        if not pad_values:
1✔
1956
            raise ValueError("array default pad must contain at least one value")
×
1957

1958
    if len(default_values) < total_size and pad_values is None and not repeat:
1✔
1959
        raise ValueError(
1✔
1960
            "array default shorter than declared x-fortran-shape without pad or repeat"
1961
        )
1962

1963
    if repeat:
1✔
1964
        pad_values = list(default_values)
1✔
1965
        if not pad_values:
1✔
UNCOV
1966
            raise ValueError("array default repeat requires at least one value")
×
1967

1968
    order_values: list[int] | None = None
1✔
1969
    if order == "C" and len(dims) > 1:
1✔
1970
        rank = len(dims)
1✔
1971
        order_values = list(range(rank, 0, -1))
1✔
1972

1973
    return ArrayDefaultSpec(
1✔
1974
        source_values=list(default_values),
1975
        pad_values=pad_values,
1976
        order_values=order_values,
1977
    )
1978

1979

1980
def _ensure_flat_scalar_list(values: list[Any], description: str) -> list[Any]:
1✔
1981
    normalized: list[Any] = []
1✔
1982
    for element in values:
1✔
1983
        if isinstance(element, list):
1✔
UNCOV
1984
            raise ValueError(f"{description} must be a flat list")
×
1985
        normalized.append(element)
1✔
1986
    return normalized
1✔
1987

1988

1989
def _enum_values(
1✔
1990
    prop: dict[str, Any],
1991
    type_info: FieldTypeInfo,
1992
    constants: dict[str, int] | None,
1993
) -> list[Any] | None:
1994
    if type_info.category == "array":
1✔
1995
        enum_raw = _array_items_enum(prop)
1✔
1996
    else:
1997
        enum_raw = prop.get("enum")
1✔
1998
    if enum_raw is None:
1✔
1999
        return None
1✔
2000
    if not isinstance(enum_raw, list) or not enum_raw:
1✔
UNCOV
2001
        raise ValueError("property enum must be a non-empty list")
×
2002
    enum_values = _ensure_flat_scalar_list(enum_raw, "enum")
1✔
2003
    category = _enum_category(type_info)
1✔
2004
    if category not in {"integer", "string"}:
1✔
UNCOV
2005
        raise ValueError("enum only supported for integer or string values")
×
2006
    for value in enum_values:
1✔
2007
        _validate_enum_scalar(value, category, "enum")
1✔
2008
    _validate_enum_defaults(prop, type_info, enum_values, category, constants)
1✔
2009
    _validate_enum_examples(prop, type_info, enum_values, category)
1✔
2010
    return enum_values
1✔
2011

2012

2013
def _bounds_spec(
1✔
2014
    prop: dict[str, Any],
2015
    type_info: FieldTypeInfo,
2016
) -> dict[str, Any] | None:
2017
    if type_info.category == "array":
1✔
2018
        bounds_prop = _array_items_bounds(prop)
1✔
2019
        category = type_info.element_category
1✔
2020
    else:
2021
        bounds_prop = prop
1✔
2022
        category = type_info.category
1✔
2023

2024
    min_value, min_exclusive = _extract_bound_value(
1✔
2025
        bounds_prop, "minimum", "exclusiveMinimum"
2026
    )
2027
    max_value, max_exclusive = _extract_bound_value(
1✔
2028
        bounds_prop, "maximum", "exclusiveMaximum"
2029
    )
2030

2031
    if min_value is None and max_value is None:
1✔
2032
        return None
1✔
2033

2034
    if category not in {"integer", "real"}:
1✔
UNCOV
2035
        raise ValueError("bounds only supported for integer or real values")
×
2036

2037
    if min_value is not None:
1✔
2038
        _validate_bound_scalar(min_value, category, "minimum")
1✔
2039
    if max_value is not None:
1✔
2040
        _validate_bound_scalar(max_value, category, "maximum")
1✔
2041

2042
    if min_value is not None and max_value is not None:
1✔
2043
        min_comp = float(min_value) if category == "real" else int(min_value)
1✔
2044
        max_comp = float(max_value) if category == "real" else int(max_value)
1✔
2045
        if min_exclusive or max_exclusive:
1✔
2046
            if min_comp >= max_comp:
1✔
2047
                raise ValueError("minimum must be less than maximum for exclusive bounds")
×
2048
        else:
UNCOV
2049
            if min_comp > max_comp:
×
UNCOV
2050
                raise ValueError("minimum must be <= maximum")
×
2051

2052
    return {
1✔
2053
        "min_value": min_value,
2054
        "min_exclusive": min_exclusive,
2055
        "max_value": max_value,
2056
        "max_exclusive": max_exclusive,
2057
        "category": category,
2058
    }
2059

2060

2061
def _extract_bound_value(
1✔
2062
    prop: dict[str, Any],
2063
    inclusive_key: str,
2064
    exclusive_key: str,
2065
) -> tuple[Any | None, bool]:
2066
    has_inclusive = inclusive_key in prop
1✔
2067
    has_exclusive = exclusive_key in prop
1✔
2068
    if has_inclusive and has_exclusive:
1✔
UNCOV
2069
        raise ValueError(
×
2070
            f"property must not define both '{inclusive_key}' and '{exclusive_key}'"
2071
        )
2072
    if has_exclusive:
1✔
2073
        value = prop.get(exclusive_key)
1✔
2074
        if value is None:
1✔
UNCOV
2075
            raise ValueError(f"{exclusive_key} must be a number")
×
2076
        return value, True
1✔
2077
    if has_inclusive:
1✔
2078
        value = prop.get(inclusive_key)
1✔
2079
        if value is None:
1✔
UNCOV
2080
            raise ValueError(f"{inclusive_key} must be a number")
×
2081
        return value, False
1✔
2082
    return None, False
1✔
2083

2084

2085
def _validate_bound_scalar(value: Any, category: str, label: str) -> None:
1✔
2086
    if category == "integer":
1✔
2087
        if isinstance(value, bool) or not isinstance(value, int):
1✔
UNCOV
2088
            raise ValueError(f"{label} must be an integer")
×
2089
        return
1✔
2090
    if category == "real":
1✔
2091
        if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
2092
            raise ValueError(f"{label} must be a number")
×
2093
        if math.isinf(float(value)):
1✔
UNCOV
2094
            raise ValueError(f"{label} must not be infinite")
×
2095
        if math.isnan(float(value)):
1✔
UNCOV
2096
            raise ValueError(f"{label} must not be NaN")
×
2097
        return
1✔
UNCOV
2098
    raise ValueError("bounds only supported for integer or real values")
×
2099

2100

2101
def _array_default_value(prop: dict[str, Any]) -> tuple[Any, bool] | None:
1✔
2102
    if prop.get("type") != "array":
1✔
UNCOV
2103
        raise ValueError("array default lookup requires array properties")
×
2104

2105
    default_defined = "default" in prop
1✔
2106
    items_default = _array_items_default(prop)
1✔
2107
    items_defined = "default" in items_default
1✔
2108
    items_value = items_default.get("default") if items_defined else None
1✔
2109

2110
    if default_defined and items_defined:
1✔
UNCOV
2111
        raise ValueError("array default must be defined on property or items, not both")
×
2112

2113
    if items_defined:
1✔
2114
        for key in ("x-fortran-default-order", "x-fortran-default-repeat", "x-fortran-default-pad"):
1✔
2115
            if key in prop:
1✔
UNCOV
2116
                raise ValueError("array items default must not use x-fortran-default-* options")
×
2117
        if isinstance(items_value, list):
1✔
UNCOV
2118
            raise ValueError("array items default must be a scalar")
×
2119
        return items_value, True
1✔
2120

2121
    if default_defined:
1✔
2122
        default_value = prop.get("default")
1✔
2123
        if not isinstance(default_value, list):
1✔
2124
            raise ValueError("array default must be a list")
1✔
2125
        return default_value, False
1✔
2126
    return None
1✔
2127

2128

2129
def _array_items_enum(prop: dict[str, Any]) -> list[Any] | None:
1✔
2130
    current = prop
1✔
2131
    while current.get("type") == "array":
1✔
2132
        if "enum" in current:
1✔
2133
            raise ValueError("array enum must be defined on items")
1✔
2134
        items = current.get("items")
1✔
2135
        if not isinstance(items, dict):
1✔
UNCOV
2136
            raise ValueError("array property must define 'items'")
×
2137
        current = items
1✔
2138
    return current.get("enum")
1✔
2139

2140

2141
def _array_items_default(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2142
    current = prop
1✔
2143
    while current.get("type") == "array":
1✔
2144
        items = current.get("items")
1✔
2145
        if not isinstance(items, dict):
1✔
UNCOV
2146
            raise ValueError("array property must define 'items'")
×
2147
        if items.get("type") == "array":
1✔
2148
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2149
        current = items
1✔
2150
    return current
1✔
2151

2152

2153
def _array_items_bounds(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2154
    current = prop
1✔
2155
    while current.get("type") == "array":
1✔
2156
        for key in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"):
1✔
2157
            if key in current:
1✔
2158
                raise ValueError("array bounds must be defined on items")
×
2159
        items = current.get("items")
1✔
2160
        if not isinstance(items, dict):
1✔
UNCOV
2161
            raise ValueError("array property must define 'items'")
×
2162
        if items.get("type") == "array":
1✔
UNCOV
2163
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2164
        current = items
1✔
2165
    return current
1✔
2166

2167

2168
def _validate_enum_scalar(value: Any, category: str, label: str) -> None:
1✔
2169
    if category == "integer":
1✔
2170
        if isinstance(value, bool) or not isinstance(value, int):
1✔
UNCOV
2171
            raise ValueError(f"{label} values must be integers")
×
2172
        return
1✔
2173
    if category == "string":
1✔
2174
        if not isinstance(value, str):
1✔
2175
            raise ValueError(f"{label} values must be strings")
×
2176
        return
1✔
UNCOV
2177
    raise ValueError("enum only supported for integer or string values")
×
2178

2179

2180
def _ensure_enum_member(value: Any, enum_values: list[Any], category: str, label: str) -> None:
1✔
UNCOV
2181
    _validate_enum_scalar(value, category, label)
×
UNCOV
2182
    if value not in enum_values:
×
2183
        raise ValueError(f"{label} value must be one of enum values")
×
2184

2185

2186
def _validate_enum_defaults(
1✔
2187
    prop: dict[str, Any],
2188
    type_info: FieldTypeInfo,
2189
    enum_values: list[Any],
2190
    category: str,
2191
    constants: dict[str, int] | None,
2192
) -> None:
2193
    if type_info.category == "array":
1✔
2194
        default_info = _array_default_value(prop)
1✔
2195
        if default_info is not None:
1✔
UNCOV
2196
            default_value, default_from_items = default_info
×
UNCOV
2197
            if default_from_items:
×
UNCOV
2198
                _ensure_enum_member(default_value, enum_values, category, "default")
×
2199
            else:
UNCOV
2200
                default_values = _ensure_flat_scalar_list(default_value, "array default")
×
UNCOV
2201
                for value in default_values:
×
UNCOV
2202
                    _ensure_enum_member(value, enum_values, category, "default")
×
2203
        pad_raw = prop.get("x-fortran-default-pad")
1✔
2204
        if pad_raw is not None:
1✔
UNCOV
2205
            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
UNCOV
2206
            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
×
UNCOV
2207
            for value in pad_values:
×
2208
                _ensure_enum_member(value, enum_values, category, "pad")
×
2209
        return
1✔
2210
    if "default" not in prop:
1✔
2211
        return
1✔
2212
    default_value = prop["default"]
×
2213
    if isinstance(default_value, list):
×
2214
        raise ValueError("scalar default must not be a list")
×
UNCOV
2215
    _ensure_enum_member(default_value, enum_values, category, "default")
×
2216

2217

2218
def _validate_enum_examples(
1✔
2219
    prop: dict[str, Any],
2220
    type_info: FieldTypeInfo,
2221
    enum_values: list[Any],
2222
    category: str,
2223
) -> None:
2224
    examples = prop.get("examples")
1✔
2225
    if examples is None:
1✔
2226
        return
1✔
2227
    if not isinstance(examples, list):
×
UNCOV
2228
        raise ValueError("property examples must be a list")
×
UNCOV
2229
    for example in examples:
×
UNCOV
2230
        if type_info.category == "array":
×
UNCOV
2231
            if isinstance(example, list):
×
UNCOV
2232
                values = _ensure_flat_scalar_list(example, "array examples")
×
UNCOV
2233
                for value in values:
×
UNCOV
2234
                    _ensure_enum_member(value, enum_values, category, "example")
×
2235
            else:
UNCOV
2236
                _ensure_enum_member(example, enum_values, category, "example")
×
2237
        else:
UNCOV
2238
            if isinstance(example, list):
×
2239
                raise ValueError("scalar examples must not be lists")
×
2240
            _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