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

MuellerSeb / nml-tools / 29619529631

17 Jul 2026 11:01PM UTC coverage: 86.477% (+0.4%) from 86.054%
29619529631

Pull #63

github

web-flow
Merge b3777c4e5 into 6717357e4
Pull Request #63: Allow defaults to satisfy required values

270 of 287 new or added lines in 7 files covered. (94.08%)

3 existing lines in 1 file now uncovered.

5039 of 5827 relevant lines covered (86.48%)

0.86 hits per line

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

86.16
/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
    validate_user_fortran_identifier,
19
)
20
from .schema import DERIVED_REF_ORIGIN_KEY
1✔
21
from .validate import (
1✔
22
    analyze_property_requirement,
23
    derived_component_defaults,
24
    validate_schema_defaults,
25
)
26

27
_TEMPLATE_ENV = Environment(
1✔
28
    loader=FileSystemLoader(Path(__file__).resolve().parent / "templates"),
29
    trim_blocks=True,
30
    lstrip_blocks=False,
31
    keep_trailing_newline=True,
32
    undefined=StrictUndefined,
33
)
34

35
_RESERVED_NAMELIST_TYPE_MEMBERS = {
1✔
36
    "filled_shape",
37
    "from_file",
38
    "init",
39
    "init_type",
40
    "is_configured",
41
    "is_set",
42
    "is_valid",
43
    "set",
44
    "set_dims",
45
}
46

47

48
@dataclass
1✔
49
class ScalarTypeInfo:
1✔
50
    """Information about a scalar Fortran type."""
51

52
    type_spec: str
1✔
53
    arg_type_spec: str
1✔
54
    kind: str | None
1✔
55
    category: str
1✔
56
    length_expr: str | None = None
1✔
57

58

59
@dataclass
1✔
60
class FieldTypeInfo:
1✔
61
    """Information about a field (scalar or array) Fortran type."""
62

63
    type_spec: str
1✔
64
    arg_type_spec: str
1✔
65
    dimensions: list[str]
1✔
66
    kind: str | None
1✔
67
    category: str
1✔
68
    length_expr: str | None = None
1✔
69
    element_category: str | None = None
1✔
70

71

72
@dataclass
1✔
73
class FieldSpec:
1✔
74
    """Information required to render a schema property."""
75

76
    order: int
1✔
77
    name: str
1✔
78
    title: str
1✔
79
    description: str | None
1✔
80
    declaration: str
1✔
81
    local_declaration: str
1✔
82
    declared_required: bool
1✔
83
    requires_input: bool
1✔
84
    sentinel_assignment: str | None
1✔
85
    sentinel_check: str | None
1✔
86
    default_assignment: str | None
1✔
87
    set_default_assignment: str | None
1✔
88
    set_present_assignment: str | None
1✔
89
    argument_declaration: str
1✔
90
    type_category: str
1✔
91
    runtime_sized_array: bool = False
1✔
92
    rank: int = 0
1✔
93

94

95
@dataclass
1✔
96
class ArrayDefaultSpec:
1✔
97
    """Normalized representation of an array default value."""
98

99
    source_values: list[Any]
1✔
100
    pad_values: list[Any] | None
1✔
101
    order_values: list[int] | None
1✔
102

103

104
@dataclass
1✔
105
class ConstantSpec:
1✔
106
    """Constant definition for helper modules."""
107

108
    name: str
1✔
109
    type_spec: str
1✔
110
    value: str
1✔
111
    doc: str | None
1✔
112

113

114
@dataclass(frozen=True)
1✔
115
class LocalDerivedTypeSpec:
1✔
116
    """A reusable locally emitted derived type declaration."""
117

118
    identity: tuple[str, str]
1✔
119
    type_name: str
1✔
120
    title: str
1✔
121
    description: str | None
1✔
122
    declarations: list[str]
1✔
123
    kind_ids: list[str]
1✔
124

125

126
def generate_fortran(
1✔
127
    schema: dict[str, Any],
128
    output: str | Path,
129
    *,
130
    helper_module: str = "nml_helper",
131
    kind_module: str | None = None,
132
    kind_map: dict[str, str] | None = None,
133
    kind_allowlist: Iterable[str] | None = None,
134
    constants: dict[str, int] | None = None,
135
    dimensions: dict[str, int] | None = None,
136
    module_doc: str | None = None,
137
    f2py_handle_helpers: bool = False,
138
) -> None:
139
    """Generate a Fortran module from *schema* at *output*."""
140
    output_path = Path(output)
1✔
141
    rendered = render_fortran(
1✔
142
        schema,
143
        file_name=output_path.name,
144
        helper_module=helper_module,
145
        kind_module=kind_module,
146
        kind_map=kind_map,
147
        kind_allowlist=kind_allowlist,
148
        constants=constants,
149
        dimensions=dimensions,
150
        module_doc=module_doc,
151
        f2py_handle_helpers=f2py_handle_helpers,
152
    )
153
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
154
    output_path.write_text(rendered, encoding="ascii")
1✔
155

156

157
def render_fortran(
1✔
158
    schema: dict[str, Any],
159
    *,
160
    file_name: str,
161
    helper_module: str = "nml_helper",
162
    kind_module: str | None = None,
163
    kind_map: dict[str, str] | None = None,
164
    kind_allowlist: Iterable[str] | None = None,
165
    constants: dict[str, int] | None = None,
166
    dimensions: dict[str, int] | None = None,
167
    module_doc: str | None = None,
168
    f2py_handle_helpers: bool = False,
169
) -> str:
170
    """Render a Fortran module from *schema*."""
171
    context = _build_context(
1✔
172
        schema,
173
        helper_module=helper_module,
174
        kind_module=kind_module,
175
        kind_map=kind_map,
176
        kind_allowlist=kind_allowlist,
177
        constants=constants,
178
        dimensions=dimensions,
179
        module_doc=module_doc,
180
        f2py_handle_helpers=f2py_handle_helpers,
181
    )
182
    context["file_name"] = file_name
1✔
183
    rendered = _TEMPLATE_ENV.get_template("fortran_module.f90.j2").render(context)
1✔
184
    return strip_trailing_whitespace(rendered)
1✔
185

186

187
def generate_helper(
1✔
188
    output: str | Path,
189
    *,
190
    module_name: str = "nml_helper",
191
    len_buf: int = 1024,
192
    constants: list[ConstantSpec] | None = None,
193
    local_derived_types: list[LocalDerivedTypeSpec] | None = None,
194
    kind_module: str | None = None,
195
    kind_map: dict[str, str] | None = None,
196
    kind_allowlist: Iterable[str] | None = None,
197
    module_doc: str | None = None,
198
    helper_header: str | None = None,
199
) -> None:
200
    """Generate the helper Fortran module at *output*."""
201
    output_path = Path(output)
×
202
    rendered = render_helper(
×
203
        file_name=output_path.name,
204
        module_name=module_name,
205
        len_buf=len_buf,
206
        constants=constants,
207
        local_derived_types=local_derived_types,
208
        kind_module=kind_module,
209
        kind_map=kind_map,
210
        kind_allowlist=kind_allowlist,
211
        module_doc=module_doc,
212
        helper_header=helper_header,
213
    )
214
    output_path.parent.mkdir(parents=True, exist_ok=True)
×
215
    output_path.write_text(rendered, encoding="ascii")
×
216

217

218
def render_helper(
1✔
219
    *,
220
    file_name: str,
221
    module_name: str = "nml_helper",
222
    len_buf: int = 1024,
223
    constants: list[ConstantSpec] | None = None,
224
    local_derived_types: list[LocalDerivedTypeSpec] | None = None,
225
    kind_module: str | None = None,
226
    kind_map: dict[str, str] | None = None,
227
    kind_allowlist: Iterable[str] | None = None,
228
    module_doc: str | None = None,
229
    helper_header: str | None = None,
230
) -> str:
231
    """Render the helper Fortran module."""
232
    if not module_name:
1✔
233
        raise ValueError("helper module name must be a non-empty string")
×
234
    if len_buf <= 0:
1✔
235
        raise ValueError("helper len_buf must be positive")
×
236
    local_types = local_derived_types or []
1✔
237
    helper_kind_ids = [
1✔
238
        kind_id for type_spec in local_types for kind_id in type_spec.kind_ids
239
    ]
240
    return _TEMPLATE_ENV.get_template("nml_helper.f90.j2").render(
1✔
241
        {
242
            "file_name": file_name,
243
            "module_name": module_name,
244
            "len_buf": len_buf,
245
            "constants": constants or [],
246
            "local_derived_types": local_types,
247
            "kind_module": kind_module or "iso_fortran_env",
248
            "kind_imports": _resolve_kind_imports(
249
                helper_kind_ids,
250
                kind_map=kind_map,
251
                kind_allowlist=kind_allowlist,
252
            ),
253
            "module_doc": module_doc,
254
            "helper_header": helper_header,
255
        }
256
    )
257

258

259
def collect_local_derived_types(
1✔
260
    schemas: Iterable[dict[str, Any]],
261
    *,
262
    constants: dict[str, int] | None = None,
263
) -> list[LocalDerivedTypeSpec]:
264
    """Collect locally owned derived definitions used by namelist schemas."""
265
    static_constants = normalize_constant_values(constants)
1✔
266
    collected: dict[tuple[str, str], LocalDerivedTypeSpec] = {}
1✔
267
    type_owners: dict[str, tuple[str, str]] = {}
1✔
268
    for schema in schemas:
1✔
269
        properties = schema.get("properties")
1✔
270
        if not isinstance(properties, dict):
1✔
271
            continue
×
272
        for prop in properties.values():
1✔
273
            if not isinstance(prop, dict):
1✔
274
                continue
×
275
            derived = _derived_schema(prop)
1✔
276
            if derived is None or derived.get("x-fortran-module") is not None:
1✔
277
                continue
1✔
278
            origin = _derived_origin(derived)
1✔
279
            identity = origin["identity"]
1✔
280
            definition = origin["definition"]
1✔
281
            type_name = _derived_type_name(definition)
1✔
282
            owner = type_owners.get(type_name.lower())
1✔
283
            if owner is not None and owner != identity:
1✔
284
                raise ValueError(
1✔
285
                    f"local derived type name '{type_name}' is used by distinct definitions"
286
                )
287
            type_owners[type_name.lower()] = identity
1✔
288
            if identity in collected:
1✔
289
                continue
1✔
290
            component_properties = definition.get("properties")
1✔
291
            if not isinstance(component_properties, dict):
1✔
292
                raise ValueError(f"derived type '{type_name}' must define properties")
×
293
            declarations: list[str] = []
1✔
294
            kind_ids: list[str] = []
1✔
295
            for name, component in component_properties.items():
1✔
296
                if not isinstance(name, str) or not isinstance(component, dict):
1✔
297
                    raise ValueError(f"derived type '{type_name}' has invalid components")
×
298
                info = _scalar_type_info(component, static_constants)
1✔
299
                if info.kind is not None:
1✔
300
                    kind_ids.append(info.kind)
1✔
301
                title = component.get("title", name)
1✔
302
                declarations.append(f"{info.type_spec} :: {name} !< {title}")
1✔
303
            title = definition.get("title", type_name)
1✔
304
            description = definition.get("description")
1✔
305
            collected[identity] = LocalDerivedTypeSpec(
1✔
306
                identity=identity,
307
                type_name=type_name,
308
                title=str(title),
309
                description=str(description) if description is not None else None,
310
                declarations=declarations,
311
                kind_ids=kind_ids,
312
            )
313
    return list(collected.values())
1✔
314

315

316
def _generated_name(*parts: str) -> str:
1✔
317
    return "__".join(parts)
1✔
318

319

320
def _build_context(
1✔
321
    schema: dict[str, Any],
322
    *,
323
    helper_module: str,
324
    kind_module: str | None,
325
    kind_map: dict[str, str] | None,
326
    kind_allowlist: Iterable[str] | None,
327
    constants: dict[str, int] | None,
328
    dimensions: dict[str, int] | None = None,
329
    module_doc: str | None = None,
330
    f2py_handle_helpers: bool = False,
331
) -> dict[str, Any]:
332
    if not helper_module:
1✔
333
        raise ValueError("helper module name must be a non-empty string")
×
334
    if "x-fortran-kind-module" in schema:
1✔
335
        raise ValueError("schema must not define 'x-fortran-kind-module'")
×
336
    namelist_name = schema.get("x-fortran-namelist")
1✔
337
    if not isinstance(namelist_name, str) or not namelist_name.strip():
1✔
338
        raise ValueError("schema must define 'x-fortran-namelist'")
×
339
    validate_user_fortran_identifier(namelist_name, label="'x-fortran-namelist'")
1✔
340

341
    if schema.get("type") != "object":
1✔
342
        raise ValueError("schema root must be of type 'object'")
×
343

344
    properties = schema.get("properties")
1✔
345
    if not isinstance(properties, dict) or not properties:
1✔
346
        raise ValueError("schema must define object 'properties'")
×
347

348
    property_items: list[tuple[str, str, dict[str, Any]]] = []
1✔
349
    property_name_map: dict[str, str] = {}
1✔
350
    for prop_name, prop in properties.items():
1✔
351
        if not isinstance(prop_name, str) or not prop_name.strip():
1✔
352
            raise ValueError("property names must be non-empty strings")
×
353
        validate_user_fortran_identifier(prop_name, label=f"property '{prop_name}'")
1✔
354
        key = prop_name.lower()
1✔
355
        if key in _RESERVED_NAMELIST_TYPE_MEMBERS:
1✔
356
            raise ValueError(
1✔
357
                f"property '{prop_name}' conflicts with generated namelist type member"
358
            )
359
        if key in property_name_map:
1✔
360
            raise ValueError(
1✔
361
                "property names must be unique (case-insensitive): "
362
                f"'{property_name_map[key]}' and '{prop_name}'"
363
            )
364
        property_name_map[key] = prop_name
1✔
365
        if not isinstance(prop, dict):
1✔
366
            raise ValueError(f"property '{prop_name}' must be an object")
×
367
        property_items.append((prop_name, key, prop))
1✔
368

369
    required_fields_raw = _ordered_unique(schema.get("required", []))
1✔
370
    required_fields: list[str] = []
1✔
371
    for req_name in required_fields_raw:
1✔
372
        if not isinstance(req_name, str):
1✔
373
            raise ValueError("schema 'required' entries must be strings")
×
374
        req_key = req_name.lower()
1✔
375
        if req_key not in property_name_map:
1✔
376
            raise ValueError(f"required property '{req_name}' is not defined")
1✔
377
        if req_key not in required_fields:
1✔
378
            required_fields.append(req_key)
1✔
379
    required_set = set(required_fields)
1✔
380
    module_name = f"nml_{namelist_name}"
1✔
381
    type_name = f"{module_name}_t"
1✔
382
    doc_class = f"{module_name}_t"
1✔
383
    brief_text = schema.get("title", namelist_name)
1✔
384
    details_text = schema.get("description", brief_text)
1✔
385

386
    fields: list[FieldSpec] = []
1✔
387
    sentinel_assignments: list[str] = []
1✔
388
    default_assignments: list[str] = []
1✔
389
    set_optional_defaults: list[str] = []
1✔
390
    local_init_assignments: list[str] = []
1✔
391
    set_required_assignments: list[str] = []
1✔
392
    presence_cases: list[dict[str, Any]] = []
1✔
393
    required_scalar_names: set[str] = set()
1✔
394
    required_array_by_name: dict[str, dict[str, Any]] = {}
1✔
395
    flex_bound_vars: set[str] = set()
1✔
396
    flex_arrays: list[dict[str, Any]] = []
1✔
397
    default_parameters: list[str] = []
1✔
398
    enum_parameters: list[str] = []
1✔
399
    enum_functions: list[dict[str, Any]] = []
1✔
400
    enum_checks: list[dict[str, Any]] = []
1✔
401
    bounds_parameters: list[str] = []
1✔
402
    bounds_functions: list[dict[str, Any]] = []
1✔
403
    bounds_checks: list[dict[str, Any]] = []
1✔
404
    derived_type_imports: list[dict[str, str]] = []
1✔
405
    derived_init_type_fields: list[dict[str, Any]] = []
1✔
406
    derived_presence_blocks: list[str] = []
1✔
407
    static_constants = normalize_constant_values(constants)
1✔
408
    runtime_dimension_values = normalize_runtime_dimensions(dimensions)
1✔
409
    reject_constant_dimension_overlap(static_constants, runtime_dimension_values)
1✔
410
    validate_schema_defaults(
1✔
411
        schema,
412
        constants=static_constants,
413
        dimensions=runtime_dimension_values,
414
    )
415
    shape_constants: dict[str, int] = {**static_constants, **runtime_dimension_values}
1✔
416
    runtime_dimensions: list[dict[str, str]] = []
1✔
417
    runtime_dimension_locals: dict[str, str] = {}
1✔
418
    runtime_default_extent_requirements: list[dict[str, Any]] = []
1✔
419
    runtime_allocations: list[str] = []
1✔
420
    runtime_deallocations: list[str] = []
1✔
421
    runtime_local_allocations: list[str] = []
1✔
422
    kind_ids: list[str] = []
1✔
423
    requires_ieee = False
1✔
424
    uses_partly_set = False
1✔
425
    helper_imports = [
1✔
426
        "nml_file_t",
427
        "nml_line_buffer",
428
        "NML_OK",
429
        "NML_ERR_FILE_NOT_FOUND",
430
        "NML_ERR_OPEN",
431
        "NML_ERR_NOT_OPEN",
432
        "NML_ERR_NML_NOT_FOUND",
433
        "NML_ERR_READ",
434
        "NML_ERR_CLOSE",
435
        "NML_ERR_REQUIRED",
436
        "NML_ERR_ENUM",
437
        "NML_ERR_BOUNDS",
438
        "NML_ERR_NOT_SET",
439
        "NML_ERR_INVALID_NAME",
440
        "NML_ERR_INVALID_INDEX",
441
        "idx_check",
442
        "to_lower",
443
    ]
444
    if f2py_handle_helpers:
1✔
445
        helper_imports.append("NML_ERR_INVALID_HANDLE")
1✔
446

447
    def _add_helper_import(name: str) -> None:
1✔
448
        if not any(existing.lower() == name.lower() for existing in helper_imports):
1✔
449
            helper_imports.append(name)
1✔
450

451
    def _unique_generated_name(base_name: str, taken_names: set[str]) -> str:
1✔
452
        if base_name.lower() not in taken_names:
1✔
453
            return base_name
1✔
454
        index = 1
×
455
        while True:
×
456
            candidate = f"{base_name}_{index}"
×
457
            if candidate.lower() not in taken_names:
×
458
                return candidate
×
459
            index += 1
×
460

461
    def _register_runtime_dimension(dim_name: str) -> str:
1✔
462
        local_name = runtime_dimension_locals.get(dim_name)
1✔
463
        if local_name is None:
1✔
464
            if dim_name in property_name_map:
1✔
465
                raise ValueError(
1✔
466
                    f"runtime dimension '{dim_name}' conflicts with property "
467
                    f"'{property_name_map[dim_name]}'"
468
                )
469
            if dim_name in _RESERVED_NAMELIST_TYPE_MEMBERS:
1✔
470
                raise ValueError(
1✔
471
                    f"runtime dimension '{dim_name}' conflicts with generated "
472
                    "namelist type member"
473
                )
474
            default_name = _generated_name(dim_name, "default")
1✔
475
            if default_name.lower() in static_constants:
1✔
476
                raise ValueError(
×
477
                    f"runtime dimension default name '{default_name}' conflicts with a constant"
478
                )
479
            local_name = dim_name
1✔
480
            runtime_dimension_locals[dim_name] = local_name
1✔
481
            _add_helper_import(default_name)
1✔
482
            runtime_dimensions.append(
1✔
483
                {
484
                    "name": dim_name,
485
                    "default_name": default_name,
486
                    "local_name": local_name,
487
                }
488
            )
489
        return local_name
1✔
490

491
    current_property: str | None = None
1✔
492
    try:
1✔
493
        for index, (display_name, attr_name, prop) in enumerate(property_items):
1✔
494
            current_property = display_name
1✔
495
            name = attr_name
1✔
496
            _reject_runtime_dimension_lengths(prop, runtime_dimension_values)
1✔
497
            type_info = _field_type_info(prop, static_constants)
1✔
498
            for const_name in _collect_dimension_constants(type_info.dimensions, shape_constants):
1✔
499
                const_key = const_name.lower()
1✔
500
                if const_key in runtime_dimension_values:
1✔
501
                    _register_runtime_dimension(const_key)
1✔
502
                else:
503
                    _add_helper_import(const_name)
1✔
504
            if type_info.length_expr and not _is_int_literal(type_info.length_expr):
1✔
505
                _add_helper_import(type_info.length_expr)
1✔
506
            if type_info.kind:
1✔
507
                kind_ids.append(type_info.kind)
1✔
508

509
            runtime_shape = list(type_info.dimensions)
1✔
510
            dynamic_shape = False
1✔
511
            if type_info.category == "array":
1✔
512
                for dim_index, dim in enumerate(runtime_shape):
1✔
513
                    if dim == ":" or _is_int_literal(dim):
1✔
514
                        continue
1✔
515
                    if not FORTRAN_IDENTIFIER.match(dim):
1✔
516
                        raise ValueError(
×
517
                            "array property 'x-fortran-shape' entries must be ints or identifiers"
518
                        )
519
                    dim_key = dim.lower()
1✔
520
                    if dim_key in runtime_dimension_values:
1✔
521
                        local_dim_name = _register_runtime_dimension(dim_key)
1✔
522
                        runtime_shape[dim_index] = f"this%{local_dim_name}"
1✔
523
                        dynamic_shape = True
1✔
524
                    elif dim_key not in static_constants:
1✔
525
                        raise ValueError(f"dimension constant '{dim}' is not defined in config")
×
526

527
            runtime_length_expr = type_info.length_expr
1✔
528
            if (
1✔
529
                type_info.length_expr
530
                and not _is_int_literal(type_info.length_expr)
531
                and type_info.length_expr.lower() in runtime_dimension_values
532
            ):
533
                raise ValueError(
×
534
                    f"dimension '{type_info.length_expr}' cannot be used as x-fortran-len"
535
                )
536
            type_spec_with_defaults = type_info.type_spec
1✔
537

538
            array_default_info: tuple[Any, bool] | None = None
1✔
539
            if type_info.category == "array":
1✔
540
                array_default_info = _array_default_value(prop)
1✔
541

542
            flex_dim = _parse_flex_dim(prop, type_info)
1✔
543
            if flex_dim > 0:
1✔
544
                if type_info.element_category == "boolean":
1✔
545
                    raise ValueError("flex arrays cannot use boolean elements")
1✔
546
                if array_default_info is not None or any(
1✔
547
                    key in prop
548
                    for key in (
549
                        "x-fortran-default-order",
550
                        "x-fortran-default-repeat",
551
                        "x-fortran-default-pad",
552
                    )
553
                ):
554
                    raise ValueError("flex arrays cannot define defaults")
×
555

556
            is_runtime_sized = type_info.category == "array" and dynamic_shape
1✔
557
            derived = _derived_schema(prop)
1✔
558

559
            if is_runtime_sized:
1✔
560
                declaration = _render_runtime_declaration(
1✔
561
                    type_info,
562
                    name,
563
                )
564
                local_decl = _render_runtime_local_declaration(
1✔
565
                    type_info,
566
                    name,
567
                    runtime_dimensions=runtime_shape,
568
                )
569
                if derived is None:
1✔
570
                    runtime_allocations.extend(
1✔
571
                        _render_runtime_allocations(
572
                            type_info,
573
                            name,
574
                            runtime_dimensions=runtime_shape,
575
                            runtime_length_expr=runtime_length_expr,
576
                            target_prefix="this%",
577
                        )
578
                    )
579
                runtime_deallocations.extend(
1✔
580
                    _render_runtime_deallocations(name, target_prefix="this%")
581
                )
582
                runtime_local_allocations.extend(
1✔
583
                    _render_runtime_local_allocations(
584
                        type_info,
585
                        name,
586
                        runtime_dimensions=runtime_shape,
587
                        runtime_length_expr=runtime_length_expr,
588
                    )
589
                )
590
            else:
591
                declaration = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
592
                local_decl = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
593

594
            title_raw = prop.get("title")
1✔
595
            if title_raw is None:
1✔
596
                title = display_name
1✔
597
            elif not isinstance(title_raw, str):
1✔
598
                raise ValueError(f"property '{display_name}' title must be a string")
×
599
            else:
600
                title = title_raw.strip() or name
1✔
601
            description = prop.get("description")
1✔
602
            declaration_with_doc = f"{declaration} !< {title}"
1✔
603

604
            dynamic_array = type_info.category == "array" and is_runtime_sized
1✔
605

606
            declared_required = name in required_set
1✔
607
            requirement = analyze_property_requirement(
1✔
608
                display_name,
609
                prop,
610
                declared_required=declared_required,
611
            )
612
            requires_input = requirement.requires_input
1✔
613
            if derived is not None:
1✔
614
                derived_type_name = _derived_type_name(derived)
1✔
615
                module = derived.get("x-fortran-module")
1✔
616
                if module is None:
1✔
617
                    _add_helper_import(derived_type_name)
1✔
618
                elif not any(
1✔
619
                    entry["module"].lower() == str(module).lower()
620
                    and entry["type_name"].lower() == derived_type_name.lower()
621
                    for entry in derived_type_imports
622
                ):
623
                    derived_type_imports.append(
1✔
624
                        {"module": str(module), "type_name": derived_type_name}
625
                    )
626

627
                components = derived.get("properties")
1✔
628
                if not isinstance(components, dict) or not components:
1✔
629
                    raise ValueError("derived object must define properties")
×
630
                inner_required = {
1✔
631
                    str(component).lower() for component in derived.get("required", [])
632
                }
633
                effective_component_defaults = derived_component_defaults(display_name, prop)
1✔
634
                leaf_entries: list[dict[str, Any]] = []
1✔
635
                init_lines: list[str] = []
1✔
636
                overlay_lines: list[str] = []
1✔
637
                for child_display_name, child in components.items():
1✔
638
                    if not isinstance(child_display_name, str) or not isinstance(child, dict):
1✔
639
                        raise ValueError("derived object components must be schema objects")
×
640
                    child_name = child_display_name.lower()
1✔
641
                    child_info = _field_type_info(child, static_constants)
1✔
642
                    if child_info.kind:
1✔
643
                        kind_ids.append(child_info.kind)
1✔
644
                    if child_info.length_expr and not _is_int_literal(child_info.length_expr):
1✔
645
                        _add_helper_import(child_info.length_expr)
×
646
                    child_target = f"this%{name}%{child_name}"
1✔
647
                    arg_target = f"{name}%{child_name}"
1✔
648
                    if (
1✔
649
                        module is not None
650
                        and child_info.category == "string"
651
                        and child_info.length_expr is not None
652
                    ):
653
                        expected_len = child_info.length_expr
1✔
654
                        storage_message = (
1✔
655
                            f"imported string storage length mismatch: {name}%{child_name}"
656
                        )
657
                        init_lines.extend(
1✔
658
                            [
659
                                f"if (len({arg_target}) /= {expected_len}) then",
660
                                "  status = NML_ERR_BOUNDS",
661
                                "  if (present(errmsg)) "
662
                                f'errmsg = "{storage_message}"',
663
                                "  return",
664
                                "end if",
665
                            ]
666
                        )
667
                    has_leaf_default = "default" in child
1✔
668
                    has_object_default = child_name in effective_component_defaults and (
1✔
669
                        not has_leaf_default
670
                        or effective_component_defaults[child_name] != child["default"]
671
                    )
672
                    has_effective_default = child_name in effective_component_defaults
1✔
673
                    if has_leaf_default:
1✔
674
                        literal = _format_default(child["default"], child_info, child, constants)
1✔
675
                        init_lines.append(f"{arg_target} = {literal}")
1✔
676
                    elif has_effective_default and child_info.category == "boolean":
1✔
677
                        literal = _format_default(
1✔
678
                            effective_component_defaults[child_name],
679
                            child_info,
680
                            child,
681
                            constants,
682
                        )
683
                        init_lines.append(f"{arg_target} = {literal}")
1✔
684
                    else:
685
                        if child_info.category == "boolean":
1✔
686
                            raise ValueError(
×
687
                                f"derived boolean component '{child_display_name}' "
688
                                "must define an effective default"
689
                            )
690
                        _, _, child_uses_ieee = _sentinel_expressions(
1✔
691
                            child_info,
692
                            var_ref=child_target,
693
                        )
694
                        arg_value_expr, _, _ = _sentinel_expressions(
1✔
695
                            child_info,
696
                            var_ref=arg_target,
697
                        )
698
                        init_lines.append(
1✔
699
                            _render_sentinel_assignment(
700
                                child_info,
701
                                target_ref=arg_target,
702
                                value_expr=arg_value_expr,
703
                                comment=f" ! sentinel for derived component {child_name}",
704
                            )
705
                        )
706
                        if child_uses_ieee:
1✔
707
                            requires_ieee = True
×
708
                    if has_effective_default:
1✔
709
                        missing_condition = None
1✔
710
                        if has_object_default:
1✔
711
                            literal = _format_default(
1✔
712
                                effective_component_defaults[child_name],
713
                                child_info,
714
                                child,
715
                                constants,
716
                            )
717
                            overlay_lines.append(f"{arg_target} = {literal}")
1✔
718
                    else:
719
                        _, missing_condition, _ = _sentinel_expressions(
1✔
720
                            child_info,
721
                            var_ref=child_target,
722
                        )
723
                    leaf_entries.append(
1✔
724
                        {
725
                            "name": child_name,
726
                            "display_name": child_display_name,
727
                            "missing_condition": missing_condition,
728
                            "required": child_name in inner_required,
729
                            "has_default": has_effective_default,
730
                        }
731
                    )
732
                    constraint_name = _generated_name(name, child_name)
1✔
733
                    component_ref = f"this%{name}%{child_name}"
1✔
734
                    enum_values = _enum_values(child, child_info, constants)
1✔
735
                    if enum_values is not None:
1✔
736
                        enum_category = _enum_category(child_info)
1✔
737
                        enum_const_name = _generated_name(constraint_name, "enum_values")
1✔
738
                        enum_literals = [
1✔
739
                            _format_scalar_default(value, child_info.kind, enum_category)
740
                            for value in enum_values
741
                        ]
742
                        if enum_category == "string":
1✔
743
                            enum_array_literal = (
1✔
744
                                f"[{child_info.type_spec} :: {', '.join(enum_literals)}]"
745
                            )
746
                            enum_parameters.append(
1✔
747
                                f"{child_info.type_spec}, parameter, public :: &\n"
748
                                f"    {enum_const_name}({len(enum_literals)}) = "
749
                                f"{enum_array_literal}"
750
                            )
751
                        else:
752
                            enum_parameters.append(
×
753
                                f"{child_info.type_spec}, parameter, public :: "
754
                                f"{enum_const_name}({len(enum_literals)}) = "
755
                                f"[{', '.join(enum_literals)}]"
756
                            )
757
                        _, enum_missing_condition, _ = _sentinel_expressions(
1✔
758
                            child_info,
759
                            var_ref="val",
760
                            len_ref="val",
761
                        )
762
                        enum_functions.append(
1✔
763
                            {
764
                                "name": constraint_name,
765
                                "func_name": _generated_name(constraint_name, "in_enum"),
766
                                "arg_type_spec": _enum_arg_type_spec(child_info),
767
                                "enum_values_name": enum_const_name,
768
                                "use_trim": enum_category == "string",
769
                                "missing_condition": enum_missing_condition,
770
                            }
771
                        )
772
                        enum_checks.append(
1✔
773
                            {
774
                                "name": constraint_name,
775
                                "display_name": f"{display_name}%{child_name}",
776
                                "func_name": _generated_name(constraint_name, "in_enum"),
777
                                "is_array": type_info.category == "array",
778
                                "runtime_array": dynamic_array,
779
                                "array_ref": component_ref,
780
                                "allocation_ref": f"this%{name}",
781
                                "element_ref": component_ref,
782
                            }
783
                        )
784
                    bounds_spec = _bounds_spec(child, child_info)
1✔
785
                    if bounds_spec is not None:
1✔
786
                        bounds_category = bounds_spec["category"]
1✔
787
                        min_value = bounds_spec["min_value"]
1✔
788
                        max_value = bounds_spec["max_value"]
1✔
789
                        min_exclusive = bounds_spec["min_exclusive"]
1✔
790
                        max_exclusive = bounds_spec["max_exclusive"]
1✔
791
                        min_name = None
1✔
792
                        max_name = None
1✔
793
                        if min_value is not None:
1✔
794
                            min_name = (
1✔
795
                                _generated_name(constraint_name, "min_excl")
796
                                if min_exclusive
797
                                else _generated_name(constraint_name, "min")
798
                            )
799
                            min_literal = _format_scalar_default(
1✔
800
                                min_value, child_info.kind, bounds_category
801
                            )
802
                            bounds_parameters.append(
1✔
803
                                f"{child_info.type_spec}, parameter, public :: "
804
                                f"{min_name} = {min_literal}"
805
                            )
806
                        if max_value is not None:
1✔
807
                            max_name = (
×
808
                                _generated_name(constraint_name, "max_excl")
809
                                if max_exclusive
810
                                else _generated_name(constraint_name, "max")
811
                            )
812
                            max_literal = _format_scalar_default(
×
813
                                max_value, child_info.kind, bounds_category
814
                            )
815
                            bounds_parameters.append(
×
816
                                f"{child_info.type_spec}, parameter, public :: "
817
                                f"{max_name} = {max_literal}"
818
                            )
819
                        _, bounds_missing_condition, child_uses_ieee = _sentinel_expressions(
1✔
820
                            child_info,
821
                            var_ref="val",
822
                            len_ref="val",
823
                        )
824
                        if child_uses_ieee:
1✔
825
                            requires_ieee = True
×
826
                        bounds_functions.append(
1✔
827
                            {
828
                                "name": constraint_name,
829
                                "func_name": _generated_name(constraint_name, "in_bounds"),
830
                                "arg_type_spec": child_info.arg_type_spec,
831
                                "has_min": min_value is not None,
832
                                "has_max": max_value is not None,
833
                                "min_name": min_name,
834
                                "max_name": max_name,
835
                                "min_exclusive": min_exclusive,
836
                                "max_exclusive": max_exclusive,
837
                                "missing_condition": bounds_missing_condition,
838
                            }
839
                        )
840
                        bounds_checks.append(
1✔
841
                            {
842
                                "name": constraint_name,
843
                                "display_name": f"{display_name}%{child_name}",
844
                                "func_name": _generated_name(constraint_name, "in_bounds"),
845
                                "is_array": type_info.category == "array",
846
                                "runtime_array": dynamic_array,
847
                                "array_ref": component_ref,
848
                                "allocation_ref": f"this%{name}",
849
                                "element_ref": component_ref,
850
                            }
851
                        )
852

853
                init_lines.extend(overlay_lines)
1✔
854
                arg_dimensions = [":" for _ in type_info.dimensions]
1✔
855
                argument_decl = _render_argument_declaration(
1✔
856
                    name=name,
857
                    type_info=type_info,
858
                    is_required=requires_input,
859
                    dimensions=arg_dimensions,
860
                    doc=title,
861
                )
862
                local_init_assignments.append(f"{name} = this%{name}")
1✔
863
                derived_partial_bounds: list[dict[str, Any]] = []
1✔
864
                set_present_assignment: str | None = None
1✔
865
                if type_info.category == "array":
1✔
866
                    for array_dim_index in range(1, len(type_info.dimensions) + 1):
1✔
867
                        lb_var, ub_var = _flex_bound_vars(array_dim_index)
1✔
868
                        derived_partial_bounds.append(
1✔
869
                            {"dim": array_dim_index, "lb_var": lb_var, "ub_var": ub_var}
870
                        )
871
                        flex_bound_vars.add(lb_var)
1✔
872
                        flex_bound_vars.add(ub_var)
1✔
873
                if requires_input:
1✔
874
                    if type_info.category == "array":
1✔
875
                        set_required_assignments.append(
1✔
876
                            _render_partial_set_block(
877
                                name, len(type_info.dimensions), derived_partial_bounds
878
                            )
879
                        )
880
                    else:
881
                        set_required_assignments.append(f"this%{name} = {name}")
1✔
882
                elif type_info.category == "array":
1✔
883
                    block = _render_partial_set_block(
1✔
884
                        name, len(type_info.dimensions), derived_partial_bounds
885
                    )
886
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
1✔
887
                    set_present_assignment = (
1✔
888
                        f"if (present({name})) then\n{indented_block}\nend if"
889
                    )
890
                else:
891
                    set_present_assignment = f"if (present({name})) this%{name} = {name}"
1✔
892

893
                init_argument_declaration = _render_argument_declaration(
1✔
894
                    name=name,
895
                    type_info=type_info,
896
                    is_required=False,
897
                    dimensions=arg_dimensions,
898
                    doc=title,
899
                )
900
                allocation_lines: list[str] = []
1✔
901
                if type_info.category == "array":
1✔
902
                    if dynamic_array:
1✔
903
                        init_argument_declaration = init_argument_declaration.replace(
1✔
904
                            ", intent(in), optional",
905
                            ", allocatable, intent(inout), optional",
906
                        )
907
                        allocation_lines.append(f"if (allocated({name})) deallocate({name})")
1✔
908
                        dims = ", ".join(runtime_shape)
1✔
909
                        allocation_lines.append(f"allocate({name}({dims}))")
1✔
910
                    else:
911
                        init_argument_declaration = init_argument_declaration.replace(
1✔
912
                            "intent(in)", "intent(inout)"
913
                        )
914
                else:
915
                    init_argument_declaration = init_argument_declaration.replace(
1✔
916
                        "intent(in)", "intent(inout)"
917
                    )
918
                derived_init_type_fields.append(
1✔
919
                    {
920
                        "name": name,
921
                        "declaration": init_argument_declaration,
922
                        "allocation_lines": allocation_lines,
923
                        "init_lines": init_lines,
924
                    }
925
                )
926
                derived_presence_blocks.extend(
1✔
927
                    _derived_presence_cases(
928
                        name=name,
929
                        display_name=display_name,
930
                        leaves=leaf_entries,
931
                        is_array=type_info.category == "array",
932
                        runtime_array=dynamic_array,
933
                        rank=len(type_info.dimensions),
934
                    )
935
                )
936
                if requires_input:
1✔
937
                    uses_partly_set = uses_partly_set or (
1✔
938
                        len(requirement.uncovered_required_components) > 1
939
                        or (
940
                            type_info.category == "array"
941
                            and bool(requirement.uncovered_required_components)
942
                        )
943
                    )
944
                fields.append(
1✔
945
                    FieldSpec(
946
                        order=index,
947
                        name=name,
948
                        title=title,
949
                        description=description,
950
                        declaration=f"{declaration} !< {title}",
951
                        local_declaration=local_decl,
952
                        declared_required=declared_required,
953
                        requires_input=requires_input,
954
                        sentinel_assignment=None,
955
                        sentinel_check=None,
956
                        default_assignment=None,
957
                        set_default_assignment=None,
958
                        set_present_assignment=set_present_assignment,
959
                        argument_declaration=argument_decl,
960
                        type_category=type_info.category,
961
                        runtime_sized_array=dynamic_array,
962
                        rank=len(type_info.dimensions),
963
                    )
964
                )
965
                continue
1✔
966
            if type_info.category == "array":
1✔
967
                has_default = array_default_info is not None
1✔
968
            else:
969
                has_default = "default" in prop
1✔
970

971
            default_from_items = False
1✔
972
            default_values: list[Any] | None = None
1✔
973
            parsed_dims: list[int] | None = None
1✔
974
            array_default_spec: ArrayDefaultSpec | None = None
1✔
975

976
            if type_info.category == "array" and has_default:
1✔
977
                if array_default_info is None:
1✔
978
                    raise ValueError(f"missing array default for '{display_name}'")
×
979
                default_raw, default_from_items = array_default_info
1✔
980
                if default_from_items:
1✔
981
                    if isinstance(default_raw, list):
1✔
982
                        raise ValueError("array items default must be a scalar")
×
983
                    default_values = [default_raw]
1✔
984
                else:
985
                    if not isinstance(default_raw, list):
1✔
986
                        raise ValueError("array default must be a list")
×
987
                    default_values = default_raw
1✔
988
                    parsed_dims = _parse_default_dimensions(type_info.dimensions, shape_constants)
1✔
989
                    array_default_spec = _prepare_array_default(default_values, parsed_dims, prop)
1✔
990

991
            needs_sentinel = not has_default
1✔
992
            requires_sentinel = needs_sentinel
1✔
993

994
            arg_dimensions = type_info.dimensions
1✔
995
            if type_info.category == "array":
1✔
996
                arg_dimensions = [":" for _ in type_info.dimensions]
1✔
997
            argument_decl = _render_argument_declaration(
1✔
998
                name=name,
999
                type_info=type_info,
1000
                is_required=requires_input,
1001
                dimensions=arg_dimensions,
1002
                doc=title,
1003
            )
1004
            local_init_assignments.append(f"{name} = this%{name}")
1✔
1005

1006
            sentinel_assignment: str | None = None
1✔
1007
            sentinel_condition: str | None = None
1✔
1008
            set_sentinel_condition: str | None = None
1✔
1009
            if requires_sentinel:
1✔
1010
                if type_info.category == "boolean":
1✔
NEW
1011
                    raise ValueError(f"boolean '{display_name}' must define a default")
×
1012
                if type_info.category == "array" and type_info.element_category == "boolean":
1✔
NEW
1013
                    raise ValueError("boolean arrays must define a default")
×
1014
                value_expr, condition_expr, uses_ieee = _sentinel_expressions(
1✔
1015
                    type_info,
1016
                    var_ref=f"this%{name}",
1017
                )
1018
                sentinel_assignment = _render_sentinel_assignment(
1✔
1019
                    type_info,
1020
                    target_ref=f"this%{name}",
1021
                    value_expr=value_expr,
1022
                    comment=_sentinel_comment(type_info, required=requires_input),
1023
                )
1024
                sentinel_condition = condition_expr
1✔
1025
                sentinel_assignments.append(sentinel_assignment)
1✔
1026
                if uses_ieee:
1✔
1027
                    requires_ieee = True
1✔
1028
                if not has_default and type_info.category != "boolean":
1✔
1029
                    set_value_expr, set_condition_expr, set_uses_ieee = _sentinel_expressions(
1✔
1030
                        type_info,
1031
                        var_ref=f"this%{name}",
1032
                    )
1033
                    if set_uses_ieee:
1✔
1034
                        requires_ieee = True
1✔
1035
                    set_sentinel_condition = set_condition_expr
1✔
1036
                    if not requires_input:
1✔
1037
                        sent_com = _sentinel_comment(type_info, required=False)
1✔
1038
                        set_optional_defaults.append(
1✔
1039
                            _render_sentinel_assignment(
1040
                                type_info,
1041
                                target_ref=f"this%{name}",
1042
                                value_expr=set_value_expr,
1043
                                comment=sent_com,
1044
                            )
1045
                        )
1046

1047
            if requires_input and type_info.category != "array":
1✔
1048
                required_scalar_names.add(name)
1✔
1049

1050
            if requires_input and type_info.category == "array" and flex_dim == 0:
1✔
1051
                element_category = type_info.element_category
1✔
1052
                if element_category is None:
1✔
1053
                    raise ValueError("array field missing element category")
×
1054
                all_missing, any_missing, uses_ieee = _array_missing_conditions(
1✔
1055
                    element_category,
1056
                    var_ref=_array_section_ref(f"this%{name}", len(type_info.dimensions)),
1057
                    len_ref=f"this%{name}",
1058
                )
1059
                required_array_by_name[name] = {
1✔
1060
                    "name": display_name,
1061
                    "attr_name": name,
1062
                    "runtime_array": dynamic_array,
1063
                    "all_missing_condition": all_missing,
1064
                    "any_missing_condition": any_missing,
1065
                }
1066
                uses_partly_set = True
1✔
1067
                if uses_ieee:
1✔
1068
                    requires_ieee = True
1✔
1069

1070
            partial_bounds: list[dict[str, Any]] | None = None
1✔
1071
            if type_info.category == "array":
1✔
1072
                rank = len(type_info.dimensions)
1✔
1073
                all_bounds = []
1✔
1074
                for array_dim_index in range(1, rank + 1):
1✔
1075
                    lb_var, ub_var = _flex_bound_vars(array_dim_index)
1✔
1076
                    all_bounds.append(
1✔
1077
                        {"dim": array_dim_index, "lb_var": lb_var, "ub_var": ub_var}
1078
                    )
1079
                    flex_bound_vars.add(lb_var)
1✔
1080
                    flex_bound_vars.add(ub_var)
1✔
1081
                partial_bounds = all_bounds
1✔
1082
            if flex_dim > 0:
1✔
1083
                rank = len(type_info.dimensions)
1✔
1084
                element_category = type_info.element_category
1✔
1085
                if element_category is None:
1✔
1086
                    raise ValueError("array field missing element category")
×
1087
                flex_dims: list[int] = list(range(rank - flex_dim + 1, rank + 1))
1✔
1088
                slice_missing_conditions: list[str] = []
1✔
1089
                slice_uses_ieee = False
1✔
1090
                flex_dim_bounds: list[dict[str, Any]] = []
1✔
1091
                lb_vars: dict[int, str] = {}
1✔
1092
                ub_vars: dict[int, str] = {}
1✔
1093
                for flex_dim_index in flex_dims:
1✔
1094
                    lb_var, ub_var = _flex_bound_vars(flex_dim_index)
1✔
1095
                    lb_vars[flex_dim_index] = lb_var
1✔
1096
                    ub_vars[flex_dim_index] = ub_var
1✔
1097
                    flex_dim_bounds.append(
1✔
1098
                        {"dim": flex_dim_index, "lb_var": lb_var, "ub_var": ub_var}
1099
                    )
1100
                    flex_bound_vars.add(lb_var)
1✔
1101
                    flex_bound_vars.add(ub_var)
1✔
1102
                    slice_ref = _slice_ref(name, rank, flex_dim_index, "idx")
1✔
1103
                    slice_missing_expr, uses_ieee = _element_missing_expression(
1✔
1104
                        element_category,
1105
                        var_ref=slice_ref,
1106
                        len_ref=f"this%{name}",
1107
                    )
1108
                    slice_missing_conditions.append(
1✔
1109
                        f"all({slice_missing_expr})" if rank > 1 else slice_missing_expr
1110
                    )
1111
                    slice_uses_ieee = slice_uses_ieee or uses_ieee
1✔
1112
                prefix_ref = _slice_ref_bounds(name, rank, flex_dims, lb_vars, ub_vars)
1✔
1113
                prefix_missing_expr, uses_ieee_prefix = _element_missing_expression(
1✔
1114
                    element_category,
1115
                    var_ref=prefix_ref,
1116
                    len_ref=f"this%{name}",
1117
                )
1118
                prefix_any_missing_condition = f"any({prefix_missing_expr})"
1✔
1119
                flex_arrays.append(
1✔
1120
                    {
1121
                        "name": name,
1122
                        "display_name": display_name,
1123
                        "rank": rank,
1124
                        "flex_dims": flex_dims,
1125
                        "required": requires_input,
1126
                        "runtime_array": dynamic_array,
1127
                        "bounds": flex_dim_bounds,
1128
                        "slice_missing_conditions": slice_missing_conditions,
1129
                        "prefix_any_missing_condition": prefix_any_missing_condition,
1130
                    }
1131
                )
1132
                uses_partly_set = True
1✔
1133
                if slice_uses_ieee or uses_ieee_prefix:
1✔
1134
                    requires_ieee = True
×
1135

1136
            default_assignment: str | None = None
1✔
1137
            set_default_assignment: str | None = None
1✔
1138
            if has_default:
1✔
1139
                default_const_name = _generated_name(name, "default")
1✔
1140
                if type_info.category == "array":
1✔
1141
                    if default_values is None:
1✔
1142
                        raise ValueError(f"missing array default for '{display_name}'")
×
1143
                    default_is_scalar = default_from_items
1✔
1144

1145
                    repeat = False
1✔
1146
                    pad_raw = None
1✔
1147
                    pad_const_name: str | None = None
1✔
1148
                    pad_is_scalar = False
1✔
1149

1150
                    if not default_from_items:
1✔
1151
                        repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
1152
                        if not isinstance(repeat_raw, bool):
1✔
1153
                            raise ValueError("array default repeat must be a boolean")
×
1154
                        repeat = bool(repeat_raw)
1✔
1155
                        pad_raw = prop.get("x-fortran-default-pad")
1✔
1156
                        if pad_raw is not None:
1✔
1157
                            pad_const_name = _generated_name(name, "pad")
1✔
1158
                            pad_is_scalar = not isinstance(pad_raw, list)
1✔
1159
                            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
1✔
1160
                            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
1✔
1161
                            pad_elements = [
1✔
1162
                                _format_scalar_default(
1163
                                    element, type_info.kind, type_info.element_category
1164
                                )
1165
                                for element in pad_values
1166
                            ]
1167
                            if pad_is_scalar:
1✔
1168
                                pad_literal = _format_scalar_default(
1✔
1169
                                    pad_raw, type_info.kind, type_info.element_category
1170
                                )
1171
                                default_parameters.append(
1✔
1172
                                    f"{type_spec_with_defaults}, parameter, public :: "
1173
                                    f"{pad_const_name} = {pad_literal}"
1174
                                )
1175
                            else:
1176
                                default_parameters.append(
×
1177
                                    f"{type_spec_with_defaults}, parameter, public :: "
1178
                                    f"{pad_const_name}({len(pad_elements)}) = "
1179
                                    f"[{', '.join(pad_elements)}]"
1180
                                )
1181

1182
                        if parsed_dims is None:
1✔
1183
                            parsed_dims = _parse_default_dimensions(
×
1184
                                type_info.dimensions, shape_constants
1185
                            )
1186
                        if array_default_spec is None:
1✔
1187
                            array_default_spec = _prepare_array_default(
×
1188
                                default_values, parsed_dims, prop
1189
                            )
1190

1191
                    if default_is_scalar:
1✔
1192
                        default_literal = _format_scalar_default(
1✔
1193
                            default_values[0], type_info.kind, type_info.element_category
1194
                        )
1195
                        default_parameters.append(
1✔
1196
                            f"{type_spec_with_defaults}, parameter, public :: "
1197
                            f"{default_const_name} = {default_literal}"
1198
                        )
1199
                    else:
1200
                        if array_default_spec is None:
1✔
1201
                            raise ValueError(
×
1202
                                f"missing array default specification for '{display_name}'"
1203
                            )
1204
                        default_elements = [
1✔
1205
                            _format_scalar_default(
1206
                                element, type_info.kind, type_info.element_category
1207
                            )
1208
                            for element in array_default_spec.source_values
1209
                        ]
1210
                        default_parameters.append(
1✔
1211
                            f"{type_spec_with_defaults}, parameter, public :: "
1212
                            f"{default_const_name}({len(default_elements)}) = "
1213
                            f"[{', '.join(default_elements)}]"
1214
                        )
1215

1216
                    if default_from_items:
1✔
1217
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
1218
                    elif (
1✔
1219
                        len(type_info.dimensions) == 1
1220
                        and array_default_spec is not None
1221
                        and array_default_spec.order_values is None
1222
                        and array_default_spec.pad_values is None
1223
                    ):
1224
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
1225
                    else:
1226
                        source_expr = default_const_name
1✔
1227
                        shape_expr = ", ".join(runtime_shape)
1✔
1228
                        arguments = [source_expr, f"shape=[{shape_expr}]"]
1✔
1229
                        if array_default_spec is not None:
1✔
1230
                            if array_default_spec.order_values is not None:
1✔
1231
                                order_literal = ", ".join(
1✔
1232
                                    str(index) for index in array_default_spec.order_values
1233
                                )
1234
                                arguments.append(f"order=[{order_literal}]")
1✔
1235
                            if array_default_spec.pad_values is not None:
1✔
1236
                                if repeat:
1✔
1237
                                    pad_expr = default_const_name
1✔
1238
                                else:
1239
                                    if pad_const_name is None:
1✔
1240
                                        raise ValueError(
×
1241
                                            f"missing pad values for array default '{display_name}'"
1242
                                        )
1243
                                    pad_expr = (
1✔
1244
                                        pad_const_name
1245
                                        if not pad_is_scalar
1246
                                        else f"[{pad_const_name}]"
1247
                                    )
1248
                                arguments.append(f"pad={pad_expr}")
1✔
1249
                        default_assignment = _format_reshape_assignment(name, arguments)
1✔
1250
                    set_default_assignment = default_assignment
1✔
1251
                else:
1252
                    default_literal = _format_default(prop["default"], type_info, prop, constants)
1✔
1253
                    default_parameters.append(
1✔
1254
                        f"{type_spec_with_defaults}, parameter, public :: "
1255
                        f"{default_const_name} = {default_literal}"
1256
                    )
1257
                    if type_info.category == "boolean":
1✔
1258
                        default_assignment = (
1✔
1259
                            f"this%{name} = {default_const_name} "
1260
                            "! bool values always need a default"
1261
                        )
1262
                    else:
1263
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
1264
                    set_default_assignment = f"this%{name} = {default_const_name}"
1✔
1265

1266
                if default_assignment is None or set_default_assignment is None:
1✔
1267
                    raise ValueError(f"missing default assignment for '{display_name}'")
×
1268
                default_assignments.append(default_assignment)
1✔
1269
                set_optional_defaults.append(set_default_assignment)
1✔
1270

1271
            if type_info.category == "array" and any(
1✔
1272
                (dim != ":") and (not _is_int_literal(dim)) for dim in type_info.dimensions
1273
            ):
1274
                required_default_elements: int | None = None
1✔
1275
                extent_mode = "minimum"
1✔
1276
                if has_default and default_values is not None:
1✔
1277
                    if default_from_items:
1✔
1278
                        required_default_elements = None
1✔
1279
                    elif array_default_spec is not None:
1✔
1280
                        required_default_elements = len(array_default_spec.source_values)
1✔
1281
                        if array_default_spec.pad_values is None:
1✔
1282
                            extent_mode = "exact"
1✔
1283
                    else:
1284
                        required_default_elements = len(default_values)
×
1285
                if required_default_elements is not None:
1✔
1286
                    runtime_default_extent_requirements.append(
1✔
1287
                        {
1288
                            "field": display_name,
1289
                            "dimensions": list(type_info.dimensions),
1290
                            "required_elements": required_default_elements,
1291
                            "mode": extent_mode,
1292
                        }
1293
                    )
1294

1295
            enum_values = _enum_values(prop, type_info, constants)
1✔
1296
            if enum_values is not None:
1✔
1297
                enum_category = _enum_category(type_info)
1✔
1298
                enum_const_name = _generated_name(name, "enum_values")
1✔
1299
                enum_literals = [
1✔
1300
                    _format_scalar_default(value, type_info.kind, enum_category)
1301
                    for value in enum_values
1302
                ]
1303
                if enum_category == "string":
1✔
1304
                    enum_array_literal = (
1✔
1305
                        f"[{type_spec_with_defaults} :: {', '.join(enum_literals)}]"
1306
                    )
1307
                else:
1308
                    enum_array_literal = f"[{', '.join(enum_literals)}]"
1✔
1309
                if enum_category == "string":
1✔
1310
                    enum_parameters.append(
1✔
1311
                        f"{type_spec_with_defaults}, parameter, public :: &\n"
1312
                        f"    {enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
1313
                    )
1314
                else:
1315
                    enum_parameters.append(
1✔
1316
                        f"{type_spec_with_defaults}, parameter, public :: "
1317
                        f"{enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
1318
                    )
1319
                enum_type_info = (
1✔
1320
                    _element_type_info(type_info) if type_info.category == "array" else type_info
1321
                )
1322
                _, missing_condition, _ = _sentinel_expressions(
1✔
1323
                    enum_type_info,
1324
                    var_ref="val",
1325
                    len_ref="val",
1326
                )
1327
                enum_functions.append(
1✔
1328
                    {
1329
                        "name": name,
1330
                        "func_name": _generated_name(name, "in_enum"),
1331
                        "arg_type_spec": _enum_arg_type_spec(type_info),
1332
                        "enum_values_name": enum_const_name,
1333
                        "use_trim": enum_category == "string",
1334
                        "missing_condition": missing_condition,
1335
                    }
1336
                )
1337
                if type_info.category == "array":
1✔
1338
                    enum_checks.append(
1✔
1339
                        {
1340
                            "name": name,
1341
                            "display_name": display_name,
1342
                            "func_name": _generated_name(name, "in_enum"),
1343
                            "is_array": True,
1344
                            "runtime_array": dynamic_array,
1345
                            "array_ref": f"this%{name}",
1346
                            "allocation_ref": f"this%{name}",
1347
                        }
1348
                    )
1349
                else:
1350
                    enum_checks.append(
1✔
1351
                        {
1352
                            "name": name,
1353
                            "display_name": display_name,
1354
                            "func_name": _generated_name(name, "in_enum"),
1355
                            "is_array": False,
1356
                            "element_ref": f"this%{name}",
1357
                        }
1358
                    )
1359

1360
            bounds_spec = _bounds_spec(prop, type_info)
1✔
1361
            if bounds_spec is not None:
1✔
1362
                bounds_category = bounds_spec["category"]
1✔
1363
                bounds_type_info = (
1✔
1364
                    _element_type_info(type_info) if type_info.category == "array" else type_info
1365
                )
1366
                min_value = bounds_spec["min_value"]
1✔
1367
                max_value = bounds_spec["max_value"]
1✔
1368
                min_exclusive = bounds_spec["min_exclusive"]
1✔
1369
                max_exclusive = bounds_spec["max_exclusive"]
1✔
1370
                min_name = None
1✔
1371
                max_name = None
1✔
1372
                if min_value is not None:
1✔
1373
                    min_name = (
1✔
1374
                        _generated_name(name, "min_excl")
1375
                        if min_exclusive
1376
                        else _generated_name(name, "min")
1377
                    )
1378
                    min_literal = _format_scalar_default(
1✔
1379
                        min_value, bounds_type_info.kind, bounds_category
1380
                    )
1381
                    bounds_parameters.append(
1✔
1382
                        f"{bounds_type_info.type_spec}, parameter, public :: "
1383
                        f"{min_name} = {min_literal}"
1384
                    )
1385
                if max_value is not None:
1✔
1386
                    max_name = (
1✔
1387
                        _generated_name(name, "max_excl")
1388
                        if max_exclusive
1389
                        else _generated_name(name, "max")
1390
                    )
1391
                    max_literal = _format_scalar_default(
1✔
1392
                        max_value, bounds_type_info.kind, bounds_category
1393
                    )
1394
                    bounds_parameters.append(
1✔
1395
                        f"{bounds_type_info.type_spec}, parameter, public :: "
1396
                        f"{max_name} = {max_literal}"
1397
                    )
1398
                _, missing_condition, uses_ieee = _sentinel_expressions(
1✔
1399
                    bounds_type_info,
1400
                    var_ref="val",
1401
                    len_ref="val",
1402
                )
1403
                if uses_ieee:
1✔
1404
                    requires_ieee = True
1✔
1405
                bounds_functions.append(
1✔
1406
                    {
1407
                        "name": name,
1408
                        "func_name": _generated_name(name, "in_bounds"),
1409
                        "arg_type_spec": bounds_type_info.arg_type_spec,
1410
                        "has_min": min_value is not None,
1411
                        "has_max": max_value is not None,
1412
                        "min_name": min_name,
1413
                        "max_name": max_name,
1414
                        "min_exclusive": min_exclusive,
1415
                        "max_exclusive": max_exclusive,
1416
                        "missing_condition": missing_condition,
1417
                    }
1418
                )
1419
                if type_info.category == "array":
1✔
1420
                    bounds_checks.append(
1✔
1421
                        {
1422
                            "name": name,
1423
                            "display_name": display_name,
1424
                            "func_name": _generated_name(name, "in_bounds"),
1425
                            "is_array": True,
1426
                            "runtime_array": dynamic_array,
1427
                            "array_ref": f"this%{name}",
1428
                            "allocation_ref": f"this%{name}",
1429
                        }
1430
                    )
1431
                else:
1432
                    bounds_checks.append(
1✔
1433
                        {
1434
                            "name": name,
1435
                            "display_name": display_name,
1436
                            "func_name": _generated_name(name, "in_bounds"),
1437
                            "is_array": False,
1438
                            "element_ref": f"this%{name}",
1439
                        }
1440
                    )
1441

1442
            is_array = type_info.category == "array"
1✔
1443
            if requires_input:
1✔
1444
                if is_array:
1✔
1445
                    # Match namelist-buffer semantics: set assigns the provided
1446
                    # leading subsection and leaves completeness checks to is_valid.
1447
                    set_required_assignments.append(
1✔
1448
                        _render_partial_set_block(
1449
                            name,
1450
                            len(type_info.dimensions),
1451
                            partial_bounds or [],
1452
                        )
1453
                    )
1454
                else:
1455
                    set_required_assignments.append(f"this%{name} = {name}")
1✔
1456

1457
            if not requires_input:
1✔
1458
                if is_array:
1✔
1459
                    # Match namelist-buffer semantics: set assigns the provided
1460
                    # leading subsection and leaves completeness checks to is_valid.
1461
                    block = _render_partial_set_block(
1✔
1462
                        name,
1463
                        len(type_info.dimensions),
1464
                        partial_bounds or [],
1465
                    )
1466
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
1✔
1467
                    set_present_assignment = (
1✔
1468
                        f"if (present({name})) then\n{indented_block}\nend if"
1469
                    )
1470
                else:
1471
                    set_present_assignment = f"if (present({name})) this%{name} = {name}"
1✔
1472
            else:
1473
                set_present_assignment = None
1✔
1474

1475
            array_rank = len(type_info.dimensions) if is_array else 0
1✔
1476
            element_condition: str | None = None
1✔
1477

1478
            if is_array and not has_default:
1✔
1479
                element_type = _element_type_info(type_info)
1✔
1480
                index_args = ", ".join(f"idx({idx})" for idx in range(1, array_rank + 1))
1✔
1481
                element_ref = f"this%{name}({index_args})"
1✔
1482
                _, element_condition, element_uses_ieee = _sentinel_expressions(
1✔
1483
                    element_type,
1484
                    var_ref=element_ref,
1485
                    len_ref=f"this%{name}",
1486
                )
1487
                if element_uses_ieee:
1✔
1488
                    requires_ieee = True
1✔
1489

1490
            if has_default or type_info.category == "boolean":
1✔
1491
                presence_cases.append(
1✔
1492
                    {
1493
                        "name": name,
1494
                        "display_name": display_name,
1495
                        "always_true": True,
1496
                        "sentinel_condition": None,
1497
                        "is_array": is_array,
1498
                        "runtime_array": dynamic_array,
1499
                        "rank": array_rank,
1500
                        "element_condition": element_condition,
1501
                    }
1502
                )
1503
            else:
1504
                if set_sentinel_condition is None:
1✔
1505
                    raise ValueError(f"missing sentinel condition for '{display_name}'")
×
1506
                presence_cases.append(
1✔
1507
                    {
1508
                        "name": name,
1509
                        "display_name": display_name,
1510
                        "always_true": False,
1511
                        "sentinel_condition": set_sentinel_condition,
1512
                        "is_array": is_array,
1513
                        "runtime_array": dynamic_array,
1514
                        "rank": array_rank,
1515
                        "element_condition": element_condition,
1516
                    }
1517
                )
1518

1519
            fields.append(
1✔
1520
                FieldSpec(
1521
                    order=index,
1522
                    name=name,
1523
                    title=title,
1524
                    description=description,
1525
                    declaration=declaration_with_doc,
1526
                    local_declaration=local_decl,
1527
                    declared_required=declared_required,
1528
                    requires_input=requires_input,
1529
                    sentinel_assignment=sentinel_assignment,
1530
                    sentinel_check=(
1531
                        f"if ({sentinel_condition}) error stop "
1532
                        f"\"{module_name}%from_file: '{name}' is required\""
1533
                        if sentinel_condition and requires_input
1534
                        else None
1535
                    ),
1536
                    default_assignment=default_assignment,
1537
                    set_default_assignment=set_default_assignment,
1538
                    set_present_assignment=set_present_assignment,
1539
                    argument_declaration=argument_decl,
1540
                    type_category=type_info.category,
1541
                    runtime_sized_array=dynamic_array,
1542
                    rank=len(type_info.dimensions),
1543
                )
1544
            )
1545

1546
    except ValueError as exc:
1✔
1547
        if current_property is None:
1✔
1548
            raise
×
1549
        msg = str(exc)
1✔
1550
        if f"property '{current_property}'" in msg:
1✔
1551
            raise
×
1552
        raise ValueError(f"property '{current_property}': {msg}") from exc
1✔
1553
    if uses_partly_set and "NML_ERR_PARTLY_SET" not in helper_imports:
1✔
1554
        helper_imports.append("NML_ERR_PARTLY_SET")
1✔
1555
    required_flex_names = {entry["name"] for entry in flex_arrays if entry["required"]}
1✔
1556
    required_input_names = [field.name for field in fields if field.requires_input]
1✔
1557
    required_scalar_validations: list[str] = []
1✔
1558
    for name in required_input_names:
1✔
1559
        if name in required_scalar_names:
1✔
1560
            required_scalar_validations.append(property_name_map[name])
1✔
1561
        elif name not in required_array_by_name and name not in required_flex_names:
1✔
1562
            required_scalar_validations.append(property_name_map[name])
1✔
1563
    required_array_validations = [
1✔
1564
        required_array_by_name[name]
1565
        for name in required_input_names
1566
        if name in required_array_by_name
1567
    ]
1568
    namelist_vars = [field.name for field in fields]
1✔
1569
    declared_required_specs = [field for field in fields if field.declared_required]
1✔
1570
    declared_optional_specs = [field for field in fields if not field.declared_required]
1✔
1571
    input_required_specs = [field for field in fields if field.requires_input]
1✔
1572
    input_optional_specs = [field for field in fields if not field.requires_input]
1✔
1573

1574
    resolved_kind_module = kind_module or "iso_fortran_env"
1✔
1575
    if not isinstance(resolved_kind_module, str) or not resolved_kind_module:
1✔
1576
        raise ValueError("kind module must be a non-empty string")
×
1577

1578
    set_dims_arguments: list[dict[str, Any]] = []
1✔
1579
    candidate_names_in_use: set[str] = set()
1✔
1580
    for entry in runtime_dimensions:
1✔
1581
        candidate_base = _generated_name("candidate", str(entry["name"]))
1✔
1582
        candidate_name = _unique_generated_name(candidate_base, candidate_names_in_use)
1✔
1583
        candidate_names_in_use.add(candidate_name.lower())
1✔
1584
        set_dims_arguments.append(
1✔
1585
            {
1586
                "name": entry["name"],
1587
                "default_name": entry["default_name"],
1588
                "local_name": entry["local_name"],
1589
                "arg_name": entry["name"],
1590
                "candidate_name": candidate_name,
1591
                "min_required": 1,
1592
            }
1593
        )
1594

1595
    candidate_name_map: dict[str, str] = {
1✔
1596
        str(entry["name"]): str(entry["candidate_name"]) for entry in set_dims_arguments
1597
    }
1598
    set_dims_extent_checks: list[dict[str, str]] = []
1✔
1599
    seen_extent_checks: set[tuple[str, str]] = set()
1✔
1600
    for extent_requirement in runtime_default_extent_requirements:
1✔
1601
        factors: list[str] = []
1✔
1602
        requirement_dimensions = cast("list[str]", extent_requirement["dimensions"])
1✔
1603
        for extent_dim in requirement_dimensions:
1✔
1604
            if extent_dim == ":":
1✔
1605
                continue
×
1606
            if _is_int_literal(extent_dim):
1✔
1607
                factors.append(extent_dim)
1✔
1608
            else:
1609
                factors.append(candidate_name_map.get(extent_dim.lower(), extent_dim))
1✔
1610
        if not factors:
1✔
1611
            continue
×
1612
        product_expr = " * ".join(factors)
1✔
1613
        if len(factors) > 1:
1✔
1614
            product_expr = f"({product_expr})"
1✔
1615
        required_elements = cast("int", extent_requirement["required_elements"])
1✔
1616
        mode = cast("str", extent_requirement["mode"])
1✔
1617
        if mode == "exact":
1✔
1618
            condition = f"{product_expr} /= {required_elements}"
1✔
1619
        else:
1620
            condition = f"{product_expr} < {required_elements}"
1✔
1621
        field_name = cast("str", extent_requirement["field"])
1✔
1622
        if mode == "exact":
1✔
1623
            extent_message = (
1✔
1624
                f"shape constants for '{field_name}' must contain exactly "
1625
                f"{required_elements} default values"
1626
            )
1627
        else:
1628
            extent_message = (
1✔
1629
                f"shape constants for '{field_name}' must allow at least "
1630
                f"{required_elements} default values"
1631
            )
1632
        extent_key = (condition, extent_message)
1✔
1633
        if extent_key in seen_extent_checks:
1✔
1634
            continue
×
1635
        seen_extent_checks.add(extent_key)
1✔
1636
        set_dims_extent_checks.append({"condition": condition, "message": extent_message})
1✔
1637

1638
    context = {
1✔
1639
        "module_name": module_name,
1640
        "type_name": type_name,
1641
        "type_prefix": module_name,
1642
        "doc_class": doc_class,
1643
        "brief_text": brief_text,
1644
        "details_text": details_text,
1645
        "module_doc": module_doc,
1646
        "namelist_name": namelist_name,
1647
        "fields": fields,
1648
        "runtime_dimensions": runtime_dimensions,
1649
        "runtime_allocations": runtime_allocations,
1650
        "runtime_deallocations": runtime_deallocations,
1651
        "runtime_local_allocations": runtime_local_allocations,
1652
        "namelist_vars": namelist_vars,
1653
        "sentinel_assignments": sentinel_assignments,
1654
        "default_assignments": default_assignments,
1655
        "default_parameters": default_parameters,
1656
        "enum_parameters": enum_parameters,
1657
        "bounds_parameters": bounds_parameters,
1658
        "local_init_assignments": local_init_assignments,
1659
        "required_scalar_validations": required_scalar_validations,
1660
        "required_array_validations": required_array_validations,
1661
        "flex_arrays": flex_arrays,
1662
        "assignments": [f"this%{field.name} = {field.name}" for field in fields],
1663
        "argument_list": [
1664
            field.name for field in declared_required_specs + declared_optional_specs
1665
        ],
1666
        "required_argument_declarations": [
1667
            field.argument_declaration for field in input_required_specs
1668
        ],
1669
        "optional_argument_declarations": [
1670
            field.argument_declaration for field in input_optional_specs
1671
        ],
1672
        "set_dims_arguments": set_dims_arguments,
1673
        "set_dims_extent_checks": set_dims_extent_checks,
1674
        "set_required_assignments": set_required_assignments,
1675
        "set_optional_defaults": set_optional_defaults,
1676
        "set_optional_present": [
1677
            field.set_present_assignment
1678
            for field in input_optional_specs
1679
            if field.set_present_assignment
1680
        ],
1681
        "enum_functions": enum_functions,
1682
        "enum_checks": enum_checks,
1683
        "bounds_functions": bounds_functions,
1684
        "bounds_checks": bounds_checks,
1685
        "derived_type_imports": derived_type_imports,
1686
        "derived_init_type_fields": derived_init_type_fields,
1687
        "derived_presence_blocks": derived_presence_blocks,
1688
        "kind_module": resolved_kind_module,
1689
        "kind_imports": _resolve_kind_imports(
1690
            kind_ids,
1691
            kind_map=kind_map,
1692
            kind_allowlist=kind_allowlist,
1693
        ),
1694
        "use_ieee": requires_ieee,
1695
        "helper_module": helper_module,
1696
        "helper_imports": helper_imports,
1697
        "presence_cases": presence_cases,
1698
        "flex_bound_vars": _sort_bound_vars(flex_bound_vars),
1699
        "f2py_handle_helpers": f2py_handle_helpers,
1700
    }
1701

1702
    return context
1✔
1703

1704

1705
def _derived_presence_cases(
1✔
1706
    *,
1707
    name: str,
1708
    display_name: str,
1709
    leaves: list[dict[str, Any]],
1710
    is_array: bool,
1711
    runtime_array: bool,
1712
    rank: int,
1713
) -> list[str]:
1714
    blocks: list[str] = []
1✔
1715
    idx_args = ", ".join(f"idx({index})" for index in range(1, rank + 1))
1✔
1716

1717
    def append_condition(
1✔
1718
        lines: list[str],
1719
        *,
1720
        prefix: str,
1721
        conditions: list[str],
1722
        operator: str,
1723
        suffix: str,
1724
    ) -> None:
1725
        if len(conditions) == 1:
1✔
1726
            lines.append(f"{prefix}{conditions[0]}{suffix}")
1✔
1727
            return
1✔
1728
        continuation = " " * (len(prefix) + 2)
1✔
1729
        for index, condition in enumerate(conditions):
1✔
1730
            if index == 0:
1✔
1731
                lines.append(f"{prefix}{condition} {operator} &")
1✔
1732
            elif index == len(conditions) - 1:
1✔
1733
                lines.append(f"{continuation}{condition}{suffix}")
1✔
1734
            else:
1735
                lines.append(f"{continuation}{condition} {operator} &")
×
1736

1737
    def array_prefix(lines: list[str]) -> None:
1✔
1738
        if runtime_array:
1✔
1739
            lines.extend(
1✔
1740
                [
1741
                    f"  if (.not. allocated(this%{name})) then",
1742
                    "    status = NML_ERR_NOT_SET",
1743
                    "    return",
1744
                    "  end if",
1745
                ]
1746
            )
1747

1748
    for leaf in leaves:
1✔
1749
        condition = leaf["missing_condition"]
1✔
1750
        lines = [f'case ("{name}%{leaf["name"]}")']
1✔
1751
        if is_array:
1✔
1752
            array_prefix(lines)
1✔
1753
            lines.extend(
1✔
1754
                [
1755
                    "  if (present(idx)) then",
1756
                    f"    status = idx_check(idx, lbound(this%{name}), ubound(this%{name}), &",
1757
                    f'      "{display_name}", errmsg)',
1758
                    "    if (status /= NML_OK) return",
1759
                ]
1760
            )
1761
            if condition is not None:
1✔
1762
                indexed = str(condition).replace(
1✔
1763
                    f"this%{name}%{leaf['name']}",
1764
                    f"this%{name}({idx_args})%{leaf['name']}",
1765
                )
1766
                lines.append(f"    if ({indexed}) status = NML_ERR_NOT_SET")
1✔
1767
            if condition is not None:
1✔
1768
                lines.append("  else")
1✔
1769
                lines.append(f"    if (all({condition})) status = NML_ERR_NOT_SET")
1✔
1770
            lines.append("  end if")
1✔
1771
        else:
1772
            lines.extend(
1✔
1773
                [
1774
                    "  if (present(idx)) then",
1775
                    "    status = NML_ERR_INVALID_INDEX",
1776
                    "    if (present(errmsg)) "
1777
                    f'errmsg = "index not supported for \'{display_name}\'"',
1778
                    "    return",
1779
                    "  end if",
1780
                ]
1781
            )
1782
            if condition is not None:
1✔
1783
                lines.append(f"  if ({condition}) status = NML_ERR_NOT_SET")
1✔
1784
        blocks.append("\n".join(lines))
1✔
1785

1786
    required_conditions = [
1✔
1787
        str(leaf["missing_condition"])
1788
        for leaf in leaves
1789
        if leaf["required"] and leaf["missing_condition"] is not None
1790
    ]
1791
    aggregate_conditions = required_conditions
1✔
1792
    lines = [f'case ("{name}")']
1✔
1793
    if is_array:
1✔
1794
        array_prefix(lines)
1✔
1795
        lines.extend(
1✔
1796
            [
1797
                "  if (present(idx)) then",
1798
                f"    status = idx_check(idx, lbound(this%{name}), ubound(this%{name}), &",
1799
                f'      "{display_name}", errmsg)',
1800
                "    if (status /= NML_OK) return",
1801
            ]
1802
        )
1803
        indexed_conditions = [
1✔
1804
            condition.replace(f"this%{name}%", f"this%{name}({idx_args})%")
1805
            for condition in aggregate_conditions
1806
        ]
1807
        if indexed_conditions:
1✔
1808
            append_condition(
1✔
1809
                lines,
1810
                prefix="    if (",
1811
                conditions=indexed_conditions,
1812
                operator=".and.",
1813
                suffix=") then",
1814
            )
1815
            lines.append("      status = NML_ERR_NOT_SET")
1✔
1816
            if required_conditions and len(indexed_conditions) > 1:
1✔
1817
                append_condition(
1✔
1818
                    lines,
1819
                    prefix="    else if (",
1820
                    conditions=indexed_conditions,
1821
                    operator=".or.",
1822
                    suffix=") then",
1823
                )
1824
                lines.append("      status = NML_ERR_PARTLY_SET")
1✔
1825
            lines.append("    end if")
1✔
1826
        if aggregate_conditions:
1✔
1827
            lines.append("  else")
1✔
1828
            append_condition(
1✔
1829
                lines,
1830
                prefix="    if (all(",
1831
                conditions=aggregate_conditions,
1832
                operator=".and.",
1833
                suffix=")) then",
1834
            )
1835
            lines.append("      status = NML_ERR_NOT_SET")
1✔
1836
            if required_conditions and (len(aggregate_conditions) > 1 or is_array):
1✔
1837
                append_condition(
1✔
1838
                    lines,
1839
                    prefix="    else if (any(",
1840
                    conditions=aggregate_conditions,
1841
                    operator=".or.",
1842
                    suffix=")) then",
1843
                )
1844
                lines.append("      status = NML_ERR_PARTLY_SET")
1✔
1845
            lines.append("    end if")
1✔
1846
        lines.append("  end if")
1✔
1847
    else:
1848
        lines.extend(
1✔
1849
            [
1850
                "  if (present(idx)) then",
1851
                "    status = NML_ERR_INVALID_INDEX",
1852
                f'    if (present(errmsg)) errmsg = "index not supported for \'{display_name}\'"',
1853
                "    return",
1854
                "  end if",
1855
            ]
1856
        )
1857
        if aggregate_conditions:
1✔
1858
            append_condition(
1✔
1859
                lines,
1860
                prefix="  if (",
1861
                conditions=aggregate_conditions,
1862
                operator=".and.",
1863
                suffix=") then",
1864
            )
1865
            lines.append("    status = NML_ERR_NOT_SET")
1✔
1866
            if len(aggregate_conditions) > 1 and required_conditions:
1✔
1867
                append_condition(
1✔
1868
                    lines,
1869
                    prefix="  else if (",
1870
                    conditions=aggregate_conditions,
1871
                    operator=".or.",
1872
                    suffix=") then",
1873
                )
1874
                lines.append("    status = NML_ERR_PARTLY_SET")
1✔
1875
            lines.append("  end if")
1✔
1876
    blocks.append("\n".join(lines))
1✔
1877
    return blocks
1✔
1878

1879

1880
def _ordered_unique(values: Iterable[Any]) -> list[Any]:
1✔
1881
    seen: set[Any] = set()
1✔
1882
    ordered: list[Any] = []
1✔
1883
    for value in values:
1✔
1884
        if value not in seen:
1✔
1885
            seen.add(value)
1✔
1886
            ordered.append(value)
1✔
1887
    return ordered
1✔
1888

1889

1890
def _reject_runtime_dimension_lengths(
1✔
1891
    prop: dict[str, Any],
1892
    dimensions: dict[str, int],
1893
) -> None:
1894
    if not dimensions:
1✔
1895
        return
1✔
1896
    if prop.get("type") == "string":
1✔
1897
        length = prop.get("x-fortran-len")
1✔
1898
        if isinstance(length, str) and length.strip().lower() in dimensions:
1✔
1899
            raise ValueError(f"dimension '{length.strip()}' cannot be used as x-fortran-len")
1✔
1900
    if prop.get("type") == "array":
1✔
1901
        items = prop.get("items")
1✔
1902
        if isinstance(items, dict):
1✔
1903
            _reject_runtime_dimension_lengths(items, dimensions)
1✔
1904

1905

1906
def _resolve_kind_imports(
1✔
1907
    kind_ids: list[str],
1908
    *,
1909
    kind_map: dict[str, str] | None,
1910
    kind_allowlist: Iterable[str] | None,
1911
) -> list[str]:
1912
    allowlist = set(kind_allowlist) if kind_allowlist is not None else None
1✔
1913
    imports: list[str] = []
1✔
1914
    target_aliases: dict[str, str] = {}
1✔
1915
    for kind_id in _ordered_unique(kind_ids):
1✔
1916
        target = kind_id
1✔
1917
        if kind_map is not None and kind_id in kind_map:
1✔
1918
            mapped = kind_map[kind_id]
1✔
1919
            if not isinstance(mapped, str):
1✔
1920
                raise ValueError(f"kind map target for '{kind_id}' must be a string")
×
1921
            target = mapped
1✔
1922
        elif allowlist is not None and kind_id not in allowlist:
1✔
1923
            raise ValueError(f"kind '{kind_id}' not present in kind map or kind module list")
×
1924

1925
        if allowlist is not None and target not in allowlist:
1✔
1926
            raise ValueError(
×
1927
                f"kind map target '{target}' for '{kind_id}' not present in kind module list"
1928
            )
1929

1930
        if target != kind_id:
1✔
1931
            existing = target_aliases.get(target)
1✔
1932
            if existing is not None and existing != kind_id:
1✔
1933
                raise ValueError(
×
1934
                    f"kind map target '{target}' is shared by '{existing}' and '{kind_id}'"
1935
                )
1936
            target_aliases[target] = kind_id
1✔
1937
            imports.append(f"{kind_id}=>{target}")
1✔
1938
        else:
1939
            imports.append(kind_id)
1✔
1940
    return imports
1✔
1941

1942

1943
def _field_type_info(
1✔
1944
    prop: dict[str, Any],
1945
    constants: dict[str, int] | None,
1946
) -> FieldTypeInfo:
1947
    prop_type = prop.get("type")
1✔
1948
    if prop_type == "array":
1✔
1949
        dimensions: list[str] = []
1✔
1950
        current = prop
1✔
1951
        while current.get("type") == "array":
1✔
1952
            dimensions.extend(_extract_dimensions(current))
1✔
1953
            items = current.get("items")
1✔
1954
            if not isinstance(items, dict):
1✔
1955
                raise ValueError("array property must define 'items'")
×
1956
            if items.get("type") == "array":
1✔
1957
                raise ValueError("nested array properties are not supported; use x-fortran-shape")
1✔
1958
            current = items
1✔
1959
        if current.get("type") == "object":
1✔
1960
            type_name = _derived_type_name(current)
1✔
1961
            return FieldTypeInfo(
1✔
1962
                type_spec=f"type({type_name})",
1963
                arg_type_spec=f"type({type_name})",
1964
                dimensions=dimensions,
1965
                kind=None,
1966
                category="array",
1967
                element_category="derived",
1968
            )
1969
        scalar = _scalar_type_info(current, constants)
1✔
1970
        return FieldTypeInfo(
1✔
1971
            type_spec=scalar.type_spec,
1972
            arg_type_spec=scalar.arg_type_spec,
1973
            dimensions=dimensions,
1974
            kind=scalar.kind,
1975
            category="array",
1976
            length_expr=scalar.length_expr,
1977
            element_category=scalar.category,
1978
        )
1979

1980
    if prop_type == "object":
1✔
1981
        type_name = _derived_type_name(prop)
1✔
1982
        return FieldTypeInfo(
1✔
1983
            type_spec=f"type({type_name})",
1984
            arg_type_spec=f"type({type_name})",
1985
            dimensions=[],
1986
            kind=None,
1987
            category="derived",
1988
        )
1989
    scalar = _scalar_type_info(prop, constants)
1✔
1990
    return FieldTypeInfo(
1✔
1991
        type_spec=scalar.type_spec,
1992
        arg_type_spec=scalar.arg_type_spec,
1993
        dimensions=[],
1994
        kind=scalar.kind,
1995
        category=scalar.category,
1996
        length_expr=scalar.length_expr,
1997
        element_category=None,
1998
    )
1999

2000

2001
def _derived_schema(prop: dict[str, Any]) -> dict[str, Any] | None:
1✔
2002
    if prop.get("type") == "object":
1✔
2003
        return prop
1✔
2004
    if prop.get("type") == "array":
1✔
2005
        items = prop.get("items")
1✔
2006
        if isinstance(items, dict) and items.get("type") == "object":
1✔
2007
            return items
1✔
2008
    return None
1✔
2009

2010

2011
def _derived_type_name(schema: dict[str, Any]) -> str:
1✔
2012
    type_name = schema.get("x-fortran-type")
1✔
2013
    if not isinstance(type_name, str) or not type_name.strip():
1✔
2014
        raise ValueError("derived object must define non-empty 'x-fortran-type'")
×
2015
    stripped = type_name.strip()
1✔
2016
    validate_user_fortran_identifier(stripped, label="'x-fortran-type'")
1✔
2017
    return stripped
1✔
2018

2019

2020
def _derived_origin(schema: dict[str, Any]) -> dict[str, Any]:
1✔
2021
    origin = schema.get(DERIVED_REF_ORIGIN_KEY)
1✔
2022
    if not isinstance(origin, dict):
1✔
2023
        raise ValueError("derived object must originate from a normalized definition")
×
2024
    raw_identity = origin.get("identity")
1✔
2025
    definition = origin.get("definition")
1✔
2026
    if (
1✔
2027
        not isinstance(raw_identity, list)
2028
        or len(raw_identity) != 2
2029
        or not all(isinstance(value, str) for value in raw_identity)
2030
        or not isinstance(definition, dict)
2031
    ):
2032
        raise ValueError("derived object has invalid reference origin metadata")
×
2033
    return {"identity": (raw_identity[0], raw_identity[1]), "definition": definition}
1✔
2034

2035

2036
def _element_type_info(type_info: FieldTypeInfo) -> FieldTypeInfo:
1✔
2037
    if type_info.category != "array":
1✔
2038
        raise ValueError("element type info requires an array field")
×
2039
    element_category = type_info.element_category
1✔
2040
    if element_category is None:
1✔
2041
        raise ValueError("array field missing element category")
×
2042
    return FieldTypeInfo(
1✔
2043
        type_spec=type_info.type_spec,
2044
        arg_type_spec=type_info.type_spec,
2045
        dimensions=[],
2046
        kind=type_info.kind,
2047
        category=element_category,
2048
        length_expr=type_info.length_expr,
2049
        element_category=None,
2050
    )
2051

2052

2053
def _enum_category(type_info: FieldTypeInfo) -> str:
1✔
2054
    if type_info.category == "array":
1✔
2055
        if type_info.element_category is None:
1✔
2056
            raise ValueError("array field missing element category")
×
2057
        return type_info.element_category
1✔
2058
    return type_info.category
1✔
2059

2060

2061
def _enum_arg_type_spec(type_info: FieldTypeInfo) -> str:
1✔
2062
    category = _enum_category(type_info)
1✔
2063
    if category == "string":
1✔
2064
        return "character(len=*)"
1✔
2065
    return type_info.type_spec
1✔
2066

2067

2068
def _scalar_type_info(
1✔
2069
    prop: dict[str, Any],
2070
    constants: dict[str, int] | None,
2071
) -> ScalarTypeInfo:
2072
    prop_type = prop.get("type")
1✔
2073
    if prop_type == "string":
1✔
2074
        length = prop.get("x-fortran-len")
1✔
2075
        if isinstance(length, bool):
1✔
2076
            raise ValueError("string property must define integer 'x-fortran-len'")
×
2077
        if isinstance(length, int):
1✔
2078
            if length <= 0:
1✔
2079
                raise ValueError("string length must be positive")
×
2080
            length_expr = str(length)
1✔
2081
        elif isinstance(length, str):
1✔
2082
            length_expr = length.strip()
1✔
2083
            if not length_expr:
1✔
2084
                raise ValueError("string length must be a non-empty value")
×
2085
            _validate_length_token(length_expr)
1✔
2086
            if _is_int_literal(length_expr):
1✔
2087
                if int(length_expr) <= 0:
×
2088
                    raise ValueError("string length must be positive")
×
2089
            else:
2090
                length_key = length_expr.lower()
1✔
2091
                if constants is None or length_key not in constants:
1✔
2092
                    raise ValueError(
×
2093
                        f"string length constant '{length_expr}' is not defined in config"
2094
                    )
2095
                value = constants[length_key]
1✔
2096
                if isinstance(value, bool) or not isinstance(value, int):
1✔
2097
                    raise ValueError(f"string length constant '{length_expr}' must be an integer")
×
2098
                if value <= 0:
1✔
2099
                    raise ValueError(f"string length constant '{length_expr}' must be positive")
×
2100
        else:
2101
            raise ValueError("string property must define integer 'x-fortran-len'")
×
2102
        return ScalarTypeInfo(
1✔
2103
            type_spec=f"character(len={length_expr})",
2104
            arg_type_spec="character(len=*)",
2105
            kind=None,
2106
            category="string",
2107
            length_expr=length_expr,
2108
        )
2109
    if prop_type == "integer":
1✔
2110
        kind = prop.get("x-fortran-kind")
1✔
2111
        if kind is None:
1✔
2112
            return ScalarTypeInfo(
1✔
2113
                type_spec="integer",
2114
                arg_type_spec="integer",
2115
                kind=None,
2116
                category="integer",
2117
            )
2118
        if not isinstance(kind, str) or not kind.strip():
1✔
2119
            raise ValueError("integer property 'x-fortran-kind' must be a non-empty string")
×
2120
        return ScalarTypeInfo(
1✔
2121
            type_spec=f"integer({kind})",
2122
            arg_type_spec=f"integer({kind})",
2123
            kind=kind,
2124
            category="integer",
2125
        )
2126
    if prop_type == "number":
1✔
2127
        kind = prop.get("x-fortran-kind")
1✔
2128
        if kind is None:
1✔
2129
            return ScalarTypeInfo(
1✔
2130
                type_spec="real",
2131
                arg_type_spec="real",
2132
                kind=None,
2133
                category="real",
2134
            )
2135
        if not isinstance(kind, str) or not kind.strip():
1✔
2136
            raise ValueError("number property 'x-fortran-kind' must be a non-empty string")
×
2137
        return ScalarTypeInfo(
1✔
2138
            type_spec=f"real({kind})",
2139
            arg_type_spec=f"real({kind})",
2140
            kind=kind,
2141
            category="real",
2142
        )
2143
    if prop_type == "boolean":
1✔
2144
        return ScalarTypeInfo(
1✔
2145
            type_spec="logical",
2146
            arg_type_spec="logical",
2147
            kind=None,
2148
            category="boolean",
2149
        )
2150
    raise ValueError(f"unsupported property type '{prop_type}'")
×
2151

2152

2153
def _extract_dimensions(prop: dict[str, Any]) -> list[str]:
1✔
2154
    shape = prop.get("x-fortran-shape")
1✔
2155
    if isinstance(shape, bool):
1✔
2156
        raise ValueError("array property 'x-fortran-shape' must not be a boolean")
×
2157
    if isinstance(shape, int):
1✔
2158
        return [str(shape)]
1✔
2159
    if isinstance(shape, str):
1✔
2160
        dim = shape.strip()
1✔
2161
        if not dim:
1✔
2162
            raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
2163
        _validate_dimension_token(dim)
1✔
2164
        return [dim]
1✔
2165
    if isinstance(shape, list):
1✔
2166
        dimensions: list[str] = []
1✔
2167
        for dim in shape:
1✔
2168
            if isinstance(dim, bool):
1✔
2169
                raise ValueError("array property 'x-fortran-shape' must not include booleans")
×
2170
            if isinstance(dim, int):
1✔
2171
                dim_literal = str(dim)
1✔
2172
            elif isinstance(dim, str):
1✔
2173
                dim_literal = dim.strip()
1✔
2174
                if not dim_literal:
1✔
2175
                    raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
2176
            else:
2177
                raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
2178
            _validate_dimension_token(dim_literal)
1✔
2179
            dimensions.append(dim_literal)
1✔
2180
        return dimensions
1✔
2181
    if shape is None:
1✔
2182
        raise ValueError("array property must define 'x-fortran-shape'")
1✔
2183
    raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
2184

2185

2186
def _is_int_literal(value: str) -> bool:
1✔
2187
    try:
1✔
2188
        int(value)
1✔
2189
    except ValueError:
1✔
2190
        return False
1✔
2191
    return True
1✔
2192

2193

2194
def _validate_dimension_token(dim: str) -> None:
1✔
2195
    if dim == ":":
1✔
2196
        return
×
2197
    if _is_int_literal(dim):
1✔
2198
        return
1✔
2199
    if FORTRAN_IDENTIFIER.match(dim):
1✔
2200
        return
1✔
2201
    raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
2202

2203

2204
def _validate_length_token(length_expr: str) -> None:
1✔
2205
    if _is_int_literal(length_expr):
1✔
2206
        return
×
2207
    if FORTRAN_IDENTIFIER.match(length_expr):
1✔
2208
        return
1✔
2209
    raise ValueError("string length must be an integer literal or identifier")
×
2210

2211

2212
def _parse_flex_dim(prop: dict[str, Any], type_info: FieldTypeInfo) -> int:
1✔
2213
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
2214
    if flex_raw is None:
1✔
2215
        flex_value = 0
1✔
2216
    else:
2217
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
1✔
2218
            raise ValueError("x-fortran-flex-tail-dims must be an integer")
×
2219
        flex_value = flex_raw
1✔
2220
    if flex_value < 0:
1✔
2221
        raise ValueError("x-fortran-flex-tail-dims must be >= 0")
×
2222
    if flex_value == 0:
1✔
2223
        return 0
1✔
2224
    if type_info.category != "array":
1✔
2225
        raise ValueError("x-fortran-flex-tail-dims is only supported for arrays")
1✔
2226
    if flex_value > len(type_info.dimensions):
1✔
2227
        raise ValueError("x-fortran-flex-tail-dims must not exceed array rank")
1✔
2228
    if any(dim == ":" for dim in type_info.dimensions):
1✔
2229
        raise ValueError(
×
2230
            "x-fortran-flex-tail-dims does not support deferred-size dimensions"
2231
        )
2232
    return flex_value
1✔
2233

2234

2235
def _collect_dimension_constants(
1✔
2236
    dimensions: list[str],
2237
    constants: dict[str, int] | None,
2238
) -> list[str]:
2239
    used: list[str] = []
1✔
2240
    for dim in dimensions:
1✔
2241
        if dim == ":" or _is_int_literal(dim):
1✔
2242
            continue
1✔
2243
        if not FORTRAN_IDENTIFIER.match(dim):
1✔
2244
            raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
2245
        dim_key = dim.lower()
1✔
2246
        if constants is None or dim_key not in constants:
1✔
2247
            raise ValueError(f"dimension constant '{dim}' is not defined in config")
1✔
2248
        value = constants[dim_key]
1✔
2249
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2250
            raise ValueError(f"dimension constant '{dim}' must be an integer")
×
2251
        if dim not in used:
1✔
2252
            used.append(dim)
1✔
2253
    return used
1✔
2254

2255

2256
def _render_declaration(type_spec: str, dimensions: list[str], name: str) -> str:
1✔
2257
    parts = [type_spec]
1✔
2258
    if dimensions:
1✔
2259
        dims = ", ".join(dimensions)
1✔
2260
        parts.append(f"dimension({dims})")
1✔
2261
    return f"{', '.join(parts)} :: {name}"
1✔
2262

2263

2264
def _render_runtime_declaration(
1✔
2265
    type_info: FieldTypeInfo,
2266
    name: str,
2267
) -> str:
2268
    if type_info.category == "string":
1✔
2269
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
2270
    if type_info.category != "array":
1✔
2271
        raise ValueError(
×
2272
            "runtime declaration is only supported for arrays, "
2273
            f"got category '{type_info.category}'"
2274
        )
2275

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

2279

2280
def _render_runtime_local_declaration(
1✔
2281
    type_info: FieldTypeInfo,
2282
    name: str,
2283
    *,
2284
    runtime_dimensions: list[str],
2285
) -> str:
2286
    if type_info.category == "string":
1✔
2287
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
2288
    if type_info.category != "array":
1✔
2289
        raise ValueError(
×
2290
            "runtime local declaration is only supported for arrays, "
2291
            f"got category '{type_info.category}'"
2292
        )
2293

2294
    type_spec = type_info.type_spec
1✔
2295
    dims = ", ".join(":" for _ in runtime_dimensions)
1✔
2296
    return f"{type_spec}, allocatable, dimension({dims}) :: {name}"
1✔
2297

2298

2299
def _render_runtime_allocations(
1✔
2300
    type_info: FieldTypeInfo,
2301
    name: str,
2302
    *,
2303
    runtime_dimensions: list[str],
2304
    runtime_length_expr: str | None,
2305
    target_prefix: str = "",
2306
) -> list[str]:
2307
    lines: list[str] = []
1✔
2308
    target_ref = f"{target_prefix}{name}"
1✔
2309
    if type_info.category != "array":
1✔
2310
        raise ValueError("runtime allocation is only supported for arrays")
×
2311
    if any(dim == ":" for dim in runtime_dimensions):
1✔
2312
        raise ValueError("runtime-sized arrays do not support deferred-size dimensions")
×
2313

2314
    lines.append(f"if (allocated({target_ref})) deallocate({target_ref})")
1✔
2315
    dims_expr = ", ".join(runtime_dimensions)
1✔
2316
    if type_info.element_category == "string" and runtime_length_expr is not None:
1✔
2317
        lines.append(f"allocate(character(len={runtime_length_expr}) :: {target_ref}({dims_expr}))")
1✔
2318
    else:
2319
        lines.append(f"allocate({target_ref}({dims_expr}))")
1✔
2320
    return lines
1✔
2321

2322

2323
def _render_runtime_deallocations(name: str, *, target_prefix: str = "") -> list[str]:
1✔
2324
    target_ref = f"{target_prefix}{name}"
1✔
2325
    return [f"if (allocated({target_ref})) deallocate({target_ref})"]
1✔
2326

2327

2328
def _render_runtime_local_allocations(
1✔
2329
    type_info: FieldTypeInfo,
2330
    name: str,
2331
    *,
2332
    runtime_dimensions: list[str],
2333
    runtime_length_expr: str | None,
2334
) -> list[str]:
2335
    return _render_runtime_allocations(
1✔
2336
        type_info,
2337
        name,
2338
        runtime_dimensions=runtime_dimensions,
2339
        runtime_length_expr=runtime_length_expr,
2340
    )
2341

2342

2343
def _array_section_ref(base_ref: str, rank: int) -> str:
1✔
2344
    dims = ", ".join(":" for _ in range(rank))
1✔
2345
    return f"{base_ref}({dims})"
1✔
2346

2347

2348
def _render_argument_declaration(
1✔
2349
    *,
2350
    name: str,
2351
    type_info: FieldTypeInfo,
2352
    is_required: bool,
2353
    dimensions: list[str] | None = None,
2354
    doc: str | None = None,
2355
) -> str:
2356
    intent = "intent(in)"
1✔
2357
    parts = [type_info.arg_type_spec]
1✔
2358
    arg_dimensions = dimensions if dimensions is not None else type_info.dimensions
1✔
2359
    if arg_dimensions:
1✔
2360
        dims = ", ".join(arg_dimensions)
1✔
2361
        parts.append(f"dimension({dims})")
1✔
2362
    if not is_required:
1✔
2363
        parts.append(intent)
1✔
2364
        parts.append("optional")
1✔
2365
        decl = f"{', '.join(parts[:-1])}, {parts[-1]} :: {name}"
1✔
2366
    else:
2367
        parts.append(intent)
1✔
2368
        decl = f"{', '.join(parts)} :: {name}"
1✔
2369
    if doc:
1✔
2370
        decl = f"{decl} !< {doc}"
1✔
2371
    return decl
1✔
2372

2373

2374
def _sentinel_comment(type_info: FieldTypeInfo, *, required: bool) -> str:
1✔
2375
    label = "required" if required else "optional"
1✔
2376
    category = type_info.category
1✔
2377
    if category == "array":
1✔
2378
        element = type_info.element_category or "array"
1✔
2379
        return f" ! sentinel for {label} {element} array"
1✔
2380
    if category == "string":
1✔
2381
        if required:
1✔
2382
            return " ! NULL string as sentinel for required string"
1✔
2383
        return " ! sentinel for optional string"
1✔
2384
    if category == "integer":
1✔
2385
        return f" ! sentinel for {label} integer"
1✔
2386
    if category == "real":
1✔
2387
        return f" ! sentinel for {label} real"
1✔
2388
    return ""
×
2389

2390

2391
def _render_sentinel_assignment(
1✔
2392
    type_info: FieldTypeInfo,
2393
    *,
2394
    target_ref: str,
2395
    value_expr: str,
2396
    comment: str,
2397
) -> str:
2398
    if type_info.category == "string":
1✔
2399
        return _render_string_sentinel_assignment(
1✔
2400
            target_ref=target_ref,
2401
            value_expr=value_expr,
2402
            comment=comment,
2403
        )
2404
    if type_info.category == "array" and type_info.element_category == "string":
1✔
2405
        return _render_string_array_sentinel_assignment(
1✔
2406
            target_ref=target_ref,
2407
            value_expr=value_expr,
2408
            comment=comment,
2409
        )
2410
    return f"{target_ref} = {value_expr}{comment}"
1✔
2411

2412

2413
def _render_string_sentinel_assignment(
1✔
2414
    *,
2415
    target_ref: str,
2416
    value_expr: str,
2417
    comment: str,
2418
) -> str:
2419
    return f"{target_ref} = {value_expr}{comment}"
1✔
2420

2421

2422
def _render_string_array_sentinel_assignment(
1✔
2423
    *,
2424
    target_ref: str,
2425
    value_expr: str,
2426
    comment: str,
2427
) -> str:
2428
    return f"{target_ref} = {value_expr}{comment}"
1✔
2429

2430

2431
def _sentinel_expressions(
1✔
2432
    type_info: FieldTypeInfo,
2433
    *,
2434
    var_ref: str,
2435
    len_ref: str | None = None,
2436
) -> tuple[str, str, bool]:
2437
    category = type_info.category
1✔
2438
    if category == "array":
1✔
2439
        element = type_info.element_category
1✔
2440
        if element == "string":
1✔
2441
            return "achar(0)", f"all({var_ref} == achar(0))", False
1✔
2442
        if element == "integer":
1✔
2443
            return f"-huge({var_ref})", f"all({var_ref} == -huge({var_ref}))", False
1✔
2444
        if element == "real":
1✔
2445
            return (
1✔
2446
                f"ieee_value({var_ref}, ieee_quiet_nan)",
2447
                f"all(ieee_is_nan({var_ref}))",
2448
                True,
2449
            )
2450
        if element == "boolean":
×
2451
            raise ValueError("boolean arrays cannot use sentinels")
×
2452
        raise ValueError(f"unsupported sentinel array element '{element}'")
×
2453
    if category == "string":
1✔
2454
        return "achar(0)", f"{var_ref} == achar(0)", False
1✔
2455
    if category == "integer":
1✔
2456
        return f"-huge({var_ref})", f"{var_ref} == -huge({var_ref})", False
1✔
2457
    if category == "real":
1✔
2458
        return (
1✔
2459
            f"ieee_value({var_ref}, ieee_quiet_nan)",
2460
            f"ieee_is_nan({var_ref})",
2461
            True,
2462
        )
2463
    if category == "boolean":
×
2464
        raise ValueError("boolean values cannot use sentinels")
×
2465
    raise ValueError(f"unsupported sentinel category '{category}'")
×
2466

2467

2468
def _element_missing_expression(
1✔
2469
    category: str,
2470
    *,
2471
    var_ref: str,
2472
    len_ref: str | None = None,
2473
) -> tuple[str, bool]:
2474
    if category == "string":
1✔
2475
        return f"{var_ref} == achar(0)", False
×
2476
    if category == "integer":
1✔
2477
        return f"{var_ref} == -huge({var_ref})", False
1✔
2478
    if category == "real":
1✔
2479
        return f"ieee_is_nan({var_ref})", True
1✔
2480
    raise ValueError(f"unsupported missing category '{category}'")
×
2481

2482

2483
def _array_missing_conditions(
1✔
2484
    element_category: str,
2485
    *,
2486
    var_ref: str,
2487
    len_ref: str | None = None,
2488
) -> tuple[str, str, bool]:
2489
    missing_expr, uses_ieee = _element_missing_expression(
1✔
2490
        element_category, var_ref=var_ref, len_ref=len_ref
2491
    )
2492
    return f"all({missing_expr})", f"any({missing_expr})", uses_ieee
1✔
2493

2494

2495
def _slice_ref(name: str, rank: int, dim: int, index_var: str) -> str:
1✔
2496
    dims = [":" for _ in range(rank)]
1✔
2497
    dims[dim - 1] = index_var
1✔
2498
    return f"this%{name}({', '.join(dims)})"
1✔
2499

2500

2501
def _flex_bound_vars(dim: int) -> tuple[str, str]:
1✔
2502
    return _generated_name("lb", str(dim)), _generated_name("ub", str(dim))
1✔
2503

2504

2505
def _slice_ref_bounds(
1✔
2506
    name: str,
2507
    rank: int,
2508
    flex_dims: list[int],
2509
    lb_vars: dict[int, str],
2510
    ub_vars: dict[int, str],
2511
) -> str:
2512
    dims: list[str] = []
1✔
2513
    for dim in range(1, rank + 1):
1✔
2514
        if dim in flex_dims:
1✔
2515
            dims.append(f"{lb_vars[dim]}:{ub_vars[dim]}")
1✔
2516
        else:
2517
            dims.append(":")
1✔
2518
    return f"this%{name}({', '.join(dims)})"
1✔
2519

2520

2521
def _render_partial_set_block(
1✔
2522
    name: str,
2523
    rank: int,
2524
    bounds: list[dict[str, Any]],
2525
) -> str:
2526
    lb_vars = {entry["dim"]: entry["lb_var"] for entry in bounds}
1✔
2527
    ub_vars = {entry["dim"]: entry["ub_var"] for entry in bounds}
1✔
2528
    dims_all = [entry["dim"] for entry in bounds]
1✔
2529
    lines: list[str] = []
1✔
2530
    for entry in bounds:
1✔
2531
        dim = entry["dim"]
1✔
2532
        lb_var = entry["lb_var"]
1✔
2533
        ub_var = entry["ub_var"]
1✔
2534
        lines.append(f"if (size({name}, {dim}) > size(this%{name}, {dim})) then")
1✔
2535
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
2536
        lines.append(
1✔
2537
            f"  if (present(errmsg)) errmsg = \"dimension {dim} exceeds bounds for '{name}'\""
2538
        )
2539
        lines.append("  return")
1✔
2540
        lines.append("end if")
1✔
2541
        lines.append(f"{lb_var} = lbound(this%{name}, {dim})")
1✔
2542
        lines.append(f"{ub_var} = {lb_var} + size({name}, {dim}) - 1")
1✔
2543
    target_ref = _slice_ref_bounds(name, rank, dims_all, lb_vars, ub_vars)
1✔
2544
    lines.append(f"{target_ref} = {name}")
1✔
2545
    return "\n".join(lines)
1✔
2546

2547

2548
def _sort_bound_vars(values: set[str]) -> list[str]:
1✔
2549
    def sort_key(name: str) -> tuple[str, int]:
1✔
2550
        prefix, _, suffix = name.partition("__")
1✔
2551
        try:
1✔
2552
            return prefix, int(suffix)
1✔
2553
        except ValueError:
×
2554
            return prefix, 0
×
2555

2556
    return sorted(values, key=sort_key)
1✔
2557

2558

2559
def _format_reshape_assignment(name: str, arguments: list[str]) -> str:
1✔
2560
    lines = [f"this%{name} = reshape( &"]
1✔
2561
    for index, arg in enumerate(arguments):
1✔
2562
        suffix = ", &" if index < len(arguments) - 1 else ")"
1✔
2563
        lines.append(f"  {arg}{suffix}")
1✔
2564
    return "\n".join(lines)
1✔
2565

2566

2567
def _format_default(
1✔
2568
    value: Any,
2569
    type_info: FieldTypeInfo,
2570
    prop: dict[str, Any],
2571
    constants: dict[str, int] | None = None,
2572
) -> str:
2573
    if type_info.category == "array":
1✔
2574
        if not isinstance(value, list):
×
2575
            raise ValueError("array default must be a list")
×
2576
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
2577
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
2578
        elements = [
×
2579
            _format_scalar_default(element, type_info.kind, type_info.element_category)
2580
            for element in array_default.source_values
2581
        ]
2582
        if (
×
2583
            len(type_info.dimensions) == 1
2584
            and array_default.order_values is None
2585
            and array_default.pad_values is None
2586
        ):
2587
            return f"[{', '.join(elements)}]"
×
2588

2589
        shape_literal = ", ".join(type_info.dimensions)
×
2590
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
2591

2592
        if array_default.order_values is not None:
×
2593
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
2594
            arguments.append(f"order=[{order_literal}]")
×
2595

2596
        if array_default.pad_values is not None:
×
2597
            pad_elements = [
×
2598
                _format_scalar_default(element, type_info.kind, type_info.element_category)
2599
                for element in array_default.pad_values
2600
            ]
2601
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
2602

2603
        return f"reshape({', '.join(arguments)})"
×
2604
    return _format_scalar_default(value, type_info.kind, type_info.category)
1✔
2605

2606

2607
def _format_scalar_default(value: Any, kind: str | None, category: str | None) -> str:
1✔
2608
    if category == "integer":
1✔
2609
        if not isinstance(value, int):
1✔
2610
            raise ValueError("integer default must be an int")
×
2611
        suffix = f"_{kind}" if kind else ""
1✔
2612
        return f"{value}{suffix}"
1✔
2613
    if category == "real":
1✔
2614
        number = float(value)
1✔
2615
        literal = repr(number)
1✔
2616
        if literal.lower() == "nan":
1✔
2617
            raise ValueError("NaN defaults are not supported")
×
2618
        if "E" in literal:
1✔
2619
            literal = literal.replace("E", "e")
×
2620
        if "." not in literal and "e" not in literal:
1✔
2621
            literal = f"{literal}.0"
×
2622
        suffix = f"_{kind}" if kind else ""
1✔
2623
        return f"{literal}{suffix}"
1✔
2624
    if category == "boolean":
1✔
2625
        if not isinstance(value, bool):
1✔
2626
            raise ValueError("boolean default must be a bool")
×
2627
        return ".true." if value else ".false."
1✔
2628
    if category == "string":
1✔
2629
        if not isinstance(value, str):
1✔
2630
            raise ValueError("string default must be a str")
×
2631
        escaped = value.replace('"', '""')
1✔
2632
        return f'"{escaped}"'
1✔
2633
    raise ValueError(f"unsupported default category '{category}'")
×
2634

2635

2636
def _parse_default_dimensions(
1✔
2637
    dimensions: list[str],
2638
    constants: dict[str, int] | None,
2639
) -> list[int]:
2640
    if not dimensions:
1✔
2641
        raise ValueError("array property missing dimensions")
×
2642
    parsed: list[int] = []
1✔
2643
    for dim in dimensions:
1✔
2644
        if dim == ":":
1✔
2645
            raise ValueError("defaults not supported for deferred-size dimensions")
×
2646
        try:
1✔
2647
            parsed.append(int(dim))
1✔
2648
        except (TypeError, ValueError) as err:  # pragma: no cover - defensive
2649
            dim_key = dim.lower()
2650
            if constants is None or dim_key not in constants:
2651
                raise ValueError(
2652
                    "array default dimensions must be integer literals or defined constants"
2653
                ) from err
2654
            value = constants[dim_key]
2655
            if isinstance(value, bool) or not isinstance(value, int):
2656
                raise ValueError("array default dimension constants must be integers") from err
2657
            parsed.append(value)
2658
    return parsed
1✔
2659

2660

2661
def _prepare_array_default(
1✔
2662
    value: list[Any],
2663
    dims: list[int],
2664
    prop: dict[str, Any],
2665
) -> ArrayDefaultSpec:
2666
    default_values = _ensure_flat_scalar_list(value, "array default")
1✔
2667
    if not default_values:
1✔
2668
        raise ValueError("array default must contain at least one value")
×
2669

2670
    total_size = math.prod(dims)
1✔
2671
    if len(default_values) > total_size:
1✔
2672
        raise ValueError("array default longer than declared x-fortran-shape")
×
2673

2674
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
2675
    if not isinstance(order_raw, str):
1✔
2676
        raise ValueError("array default order must be 'F' or 'C'")
×
2677
    order = order_raw.upper()
1✔
2678
    if order not in {"F", "C"}:
1✔
2679
        raise ValueError("array default order must be 'F' or 'C'")
×
2680

2681
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
2682
    if not isinstance(repeat_raw, bool):
1✔
2683
        raise ValueError("array default repeat must be a boolean")
×
2684
    repeat = bool(repeat_raw)
1✔
2685

2686
    pad_raw = prop.get("x-fortran-default-pad")
1✔
2687
    pad_values: list[Any] | None = None
1✔
2688
    if pad_raw is not None:
1✔
2689
        if repeat:
1✔
2690
            raise ValueError("array default cannot set both pad and repeat")
×
2691
        if not isinstance(pad_raw, list):
1✔
2692
            pad_raw = [pad_raw]
1✔
2693
        pad_values = _ensure_flat_scalar_list(pad_raw, "array default pad")
1✔
2694
        if not pad_values:
1✔
2695
            raise ValueError("array default pad must contain at least one value")
×
2696

2697
    if len(default_values) < total_size and pad_values is None and not repeat:
1✔
2698
        raise ValueError(
×
2699
            "array default shorter than declared x-fortran-shape without pad or repeat"
2700
        )
2701

2702
    if repeat:
1✔
2703
        pad_values = list(default_values)
1✔
2704
        if not pad_values:
1✔
2705
            raise ValueError("array default repeat requires at least one value")
×
2706

2707
    order_values: list[int] | None = None
1✔
2708
    if order == "C" and len(dims) > 1:
1✔
2709
        rank = len(dims)
1✔
2710
        order_values = list(range(rank, 0, -1))
1✔
2711

2712
    return ArrayDefaultSpec(
1✔
2713
        source_values=list(default_values),
2714
        pad_values=pad_values,
2715
        order_values=order_values,
2716
    )
2717

2718

2719
def _ensure_flat_scalar_list(values: list[Any], description: str) -> list[Any]:
1✔
2720
    normalized: list[Any] = []
1✔
2721
    for element in values:
1✔
2722
        if isinstance(element, list):
1✔
2723
            raise ValueError(f"{description} must be a flat list")
×
2724
        normalized.append(element)
1✔
2725
    return normalized
1✔
2726

2727

2728
def _enum_values(
1✔
2729
    prop: dict[str, Any],
2730
    type_info: FieldTypeInfo,
2731
    constants: dict[str, int] | None,
2732
) -> list[Any] | None:
2733
    if type_info.category == "array":
1✔
2734
        enum_raw = _array_items_enum(prop)
1✔
2735
    else:
2736
        enum_raw = prop.get("enum")
1✔
2737
    if enum_raw is None:
1✔
2738
        return None
1✔
2739
    if not isinstance(enum_raw, list) or not enum_raw:
1✔
2740
        raise ValueError("property enum must be a non-empty list")
×
2741
    enum_values = _ensure_flat_scalar_list(enum_raw, "enum")
1✔
2742
    category = _enum_category(type_info)
1✔
2743
    if category not in {"integer", "string"}:
1✔
2744
        raise ValueError("enum only supported for integer or string values")
×
2745
    for value in enum_values:
1✔
2746
        _validate_enum_scalar(value, category, "enum")
1✔
2747
    _validate_enum_defaults(prop, type_info, enum_values, category, constants)
1✔
2748
    _validate_enum_examples(prop, type_info, enum_values, category)
1✔
2749
    return enum_values
1✔
2750

2751

2752
def _bounds_spec(
1✔
2753
    prop: dict[str, Any],
2754
    type_info: FieldTypeInfo,
2755
) -> dict[str, Any] | None:
2756
    if type_info.category == "array":
1✔
2757
        bounds_prop = _array_items_bounds(prop)
1✔
2758
        category = type_info.element_category
1✔
2759
    else:
2760
        bounds_prop = prop
1✔
2761
        category = type_info.category
1✔
2762

2763
    min_value, min_exclusive = _extract_bound_value(
1✔
2764
        bounds_prop, "minimum", "exclusiveMinimum"
2765
    )
2766
    max_value, max_exclusive = _extract_bound_value(
1✔
2767
        bounds_prop, "maximum", "exclusiveMaximum"
2768
    )
2769

2770
    if min_value is None and max_value is None:
1✔
2771
        return None
1✔
2772

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

2776
    if min_value is not None:
1✔
2777
        _validate_bound_scalar(min_value, category, "minimum")
1✔
2778
    if max_value is not None:
1✔
2779
        _validate_bound_scalar(max_value, category, "maximum")
1✔
2780

2781
    if min_value is not None and max_value is not None:
1✔
2782
        min_comp = float(min_value) if category == "real" else int(min_value)
1✔
2783
        max_comp = float(max_value) if category == "real" else int(max_value)
1✔
2784
        if min_exclusive or max_exclusive:
1✔
2785
            if min_comp >= max_comp:
1✔
2786
                raise ValueError("minimum must be less than maximum for exclusive bounds")
×
2787
        else:
2788
            if min_comp > max_comp:
×
2789
                raise ValueError("minimum must be <= maximum")
×
2790

2791
    return {
1✔
2792
        "min_value": min_value,
2793
        "min_exclusive": min_exclusive,
2794
        "max_value": max_value,
2795
        "max_exclusive": max_exclusive,
2796
        "category": category,
2797
    }
2798

2799

2800
def _extract_bound_value(
1✔
2801
    prop: dict[str, Any],
2802
    inclusive_key: str,
2803
    exclusive_key: str,
2804
) -> tuple[Any | None, bool]:
2805
    has_inclusive = inclusive_key in prop
1✔
2806
    has_exclusive = exclusive_key in prop
1✔
2807
    if has_inclusive and has_exclusive:
1✔
2808
        raise ValueError(
×
2809
            f"property must not define both '{inclusive_key}' and '{exclusive_key}'"
2810
        )
2811
    if has_exclusive:
1✔
2812
        value = prop.get(exclusive_key)
1✔
2813
        if value is None:
1✔
2814
            raise ValueError(f"{exclusive_key} must be a number")
×
2815
        return value, True
1✔
2816
    if has_inclusive:
1✔
2817
        value = prop.get(inclusive_key)
1✔
2818
        if value is None:
1✔
2819
            raise ValueError(f"{inclusive_key} must be a number")
×
2820
        return value, False
1✔
2821
    return None, False
1✔
2822

2823

2824
def _validate_bound_scalar(value: Any, category: str, label: str) -> None:
1✔
2825
    if category == "integer":
1✔
2826
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2827
            raise ValueError(f"{label} must be an integer")
×
2828
        return
1✔
2829
    if category == "real":
1✔
2830
        if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
2831
            raise ValueError(f"{label} must be a number")
×
2832
        if math.isinf(float(value)):
1✔
2833
            raise ValueError(f"{label} must not be infinite")
×
2834
        if math.isnan(float(value)):
1✔
2835
            raise ValueError(f"{label} must not be NaN")
×
2836
        return
1✔
2837
    raise ValueError("bounds only supported for integer or real values")
×
2838

2839

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

2844
    default_defined = "default" in prop
1✔
2845
    items_default = _array_items_default(prop)
1✔
2846
    items_defined = "default" in items_default
1✔
2847
    items_value = items_default.get("default") if items_defined else None
1✔
2848

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

2852
    if items_defined:
1✔
2853
        for key in ("x-fortran-default-order", "x-fortran-default-repeat", "x-fortran-default-pad"):
1✔
2854
            if key in prop:
1✔
2855
                raise ValueError("array items default must not use x-fortran-default-* options")
×
2856
        if isinstance(items_value, list):
1✔
2857
            raise ValueError("array items default must be a scalar")
×
2858
        return items_value, True
1✔
2859

2860
    if default_defined:
1✔
2861
        default_value = prop.get("default")
1✔
2862
        if not isinstance(default_value, list):
1✔
2863
            raise ValueError("array default must be a list")
×
2864
        return default_value, False
1✔
2865
    return None
1✔
2866

2867

2868
def _array_items_enum(prop: dict[str, Any]) -> list[Any] | None:
1✔
2869
    current = prop
1✔
2870
    while current.get("type") == "array":
1✔
2871
        if "enum" in current:
1✔
2872
            raise ValueError("array enum must be defined on items")
1✔
2873
        items = current.get("items")
1✔
2874
        if not isinstance(items, dict):
1✔
2875
            raise ValueError("array property must define 'items'")
×
2876
        current = items
1✔
2877
    return current.get("enum")
1✔
2878

2879

2880
def _array_items_default(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2881
    current = prop
1✔
2882
    while current.get("type") == "array":
1✔
2883
        items = current.get("items")
1✔
2884
        if not isinstance(items, dict):
1✔
2885
            raise ValueError("array property must define 'items'")
×
2886
        if items.get("type") == "array":
1✔
2887
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2888
        current = items
1✔
2889
    return current
1✔
2890

2891

2892
def _array_items_bounds(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2893
    current = prop
1✔
2894
    while current.get("type") == "array":
1✔
2895
        for key in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"):
1✔
2896
            if key in current:
1✔
2897
                raise ValueError("array bounds must be defined on items")
×
2898
        items = current.get("items")
1✔
2899
        if not isinstance(items, dict):
1✔
2900
            raise ValueError("array property must define 'items'")
×
2901
        if items.get("type") == "array":
1✔
2902
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2903
        current = items
1✔
2904
    return current
1✔
2905

2906

2907
def _validate_enum_scalar(value: Any, category: str, label: str) -> None:
1✔
2908
    if category == "integer":
1✔
2909
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2910
            raise ValueError(f"{label} values must be integers")
×
2911
        return
1✔
2912
    if category == "string":
1✔
2913
        if not isinstance(value, str):
1✔
2914
            raise ValueError(f"{label} values must be strings")
×
2915
        return
1✔
2916
    raise ValueError("enum only supported for integer or string values")
×
2917

2918

2919
def _ensure_enum_member(value: Any, enum_values: list[Any], category: str, label: str) -> None:
1✔
2920
    _validate_enum_scalar(value, category, label)
1✔
2921
    if value not in enum_values:
1✔
2922
        raise ValueError(f"{label} value must be one of enum values")
×
2923

2924

2925
def _validate_enum_defaults(
1✔
2926
    prop: dict[str, Any],
2927
    type_info: FieldTypeInfo,
2928
    enum_values: list[Any],
2929
    category: str,
2930
    constants: dict[str, int] | None,
2931
) -> None:
2932
    if type_info.category == "array":
1✔
2933
        default_info = _array_default_value(prop)
1✔
2934
        if default_info is not None:
1✔
2935
            default_value, default_from_items = default_info
×
2936
            if default_from_items:
×
2937
                _ensure_enum_member(default_value, enum_values, category, "default")
×
2938
            else:
2939
                default_values = _ensure_flat_scalar_list(default_value, "array default")
×
2940
                for value in default_values:
×
2941
                    _ensure_enum_member(value, enum_values, category, "default")
×
2942
        pad_raw = prop.get("x-fortran-default-pad")
1✔
2943
        if pad_raw is not None:
1✔
2944
            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
2945
            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
×
2946
            for value in pad_values:
×
2947
                _ensure_enum_member(value, enum_values, category, "pad")
×
2948
        return
1✔
2949
    if "default" not in prop:
1✔
2950
        return
1✔
2951
    default_value = prop["default"]
1✔
2952
    if isinstance(default_value, list):
1✔
2953
        raise ValueError("scalar default must not be a list")
×
2954
    _ensure_enum_member(default_value, enum_values, category, "default")
1✔
2955

2956

2957
def _validate_enum_examples(
1✔
2958
    prop: dict[str, Any],
2959
    type_info: FieldTypeInfo,
2960
    enum_values: list[Any],
2961
    category: str,
2962
) -> None:
2963
    examples = prop.get("examples")
1✔
2964
    if examples is None:
1✔
2965
        return
1✔
2966
    if not isinstance(examples, list):
×
2967
        raise ValueError("property examples must be a list")
×
2968
    for example in examples:
×
2969
        if type_info.category == "array":
×
2970
            if isinstance(example, list):
×
2971
                values = _ensure_flat_scalar_list(example, "array examples")
×
2972
                for value in values:
×
2973
                    _ensure_enum_member(value, enum_values, category, "example")
×
2974
            else:
2975
                _ensure_enum_member(example, enum_values, category, "example")
×
2976
        else:
2977
            if isinstance(example, list):
×
2978
                raise ValueError("scalar examples must not be lists")
×
2979
            _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