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

MuellerSeb / nml-tools / 26509481874

27 May 2026 11:52AM UTC coverage: 83.53% (+2.5%) from 81.064%
26509481874

Pull #32

github

MuellerSeb
Reject non-scalar derived members during raw validation

Apply the derived-type v1 intrinsic-scalar member restriction in validate_schema_defaults so callers validating in-memory, unresolved schema dictionaries receive the same rejection as schema resolution and code generation.

Add regression cases for raw derived declarations containing array and nested-object members.
Pull Request #32: Add One-Level Derived-Type Support For Generated Namelists

690 of 760 new or added lines in 7 files covered. (90.79%)

1 existing line in 1 file now uncovered.

3743 of 4481 relevant lines covered (83.53%)

0.84 hits per line

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

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

3
from __future__ import annotations
1✔
4

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

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

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

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

30

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

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

41

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

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

54

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

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

76

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

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

85

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

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

95

96
@dataclass(frozen=True)
1✔
97
class LocalDerivedTypeSpec:
1✔
98
    """A reusable locally emitted derived type declaration."""
99

100
    identity: tuple[str, str]
1✔
101
    type_name: str
1✔
102
    title: str
1✔
103
    description: str | None
1✔
104
    declarations: list[str]
1✔
105
    kind_ids: list[str]
1✔
106

107

108
def generate_fortran(
1✔
109
    schema: dict[str, Any],
110
    output: str | Path,
111
    *,
112
    helper_module: str = "nml_helper",
113
    kind_module: str | None = None,
114
    kind_map: dict[str, str] | None = None,
115
    kind_allowlist: Iterable[str] | None = None,
116
    constants: dict[str, int] | None = None,
117
    dimensions: dict[str, int] | None = None,
118
    module_doc: str | None = None,
119
    f2py_handle_helpers: bool = False,
120
) -> None:
121
    """Generate a Fortran module from *schema* at *output*."""
122
    output_path = Path(output)
1✔
123
    rendered = render_fortran(
1✔
124
        schema,
125
        file_name=output_path.name,
126
        helper_module=helper_module,
127
        kind_module=kind_module,
128
        kind_map=kind_map,
129
        kind_allowlist=kind_allowlist,
130
        constants=constants,
131
        dimensions=dimensions,
132
        module_doc=module_doc,
133
        f2py_handle_helpers=f2py_handle_helpers,
134
    )
135
    output_path.parent.mkdir(parents=True, exist_ok=True)
1✔
136
    output_path.write_text(rendered, encoding="ascii")
1✔
137

138

139
def render_fortran(
1✔
140
    schema: dict[str, Any],
141
    *,
142
    file_name: str,
143
    helper_module: str = "nml_helper",
144
    kind_module: str | None = None,
145
    kind_map: dict[str, str] | None = None,
146
    kind_allowlist: Iterable[str] | None = None,
147
    constants: dict[str, int] | None = None,
148
    dimensions: dict[str, int] | None = None,
149
    module_doc: str | None = None,
150
    f2py_handle_helpers: bool = False,
151
) -> str:
152
    """Render a Fortran module from *schema*."""
153
    context = _build_context(
1✔
154
        schema,
155
        helper_module=helper_module,
156
        kind_module=kind_module,
157
        kind_map=kind_map,
158
        kind_allowlist=kind_allowlist,
159
        constants=constants,
160
        dimensions=dimensions,
161
        module_doc=module_doc,
162
        f2py_handle_helpers=f2py_handle_helpers,
163
    )
164
    context["file_name"] = file_name
1✔
165
    rendered = _TEMPLATE_ENV.get_template("fortran_module.f90.j2").render(context)
1✔
166
    return strip_trailing_whitespace(rendered)
1✔
167

168

169
def generate_helper(
1✔
170
    output: str | Path,
171
    *,
172
    module_name: str = "nml_helper",
173
    len_buf: int = 1024,
174
    constants: list[ConstantSpec] | None = None,
175
    local_derived_types: list[LocalDerivedTypeSpec] | None = None,
176
    kind_module: str | None = None,
177
    kind_map: dict[str, str] | None = None,
178
    kind_allowlist: Iterable[str] | None = None,
179
    module_doc: str | None = None,
180
    helper_header: str | None = None,
181
) -> None:
182
    """Generate the helper Fortran module at *output*."""
183
    output_path = Path(output)
×
184
    rendered = render_helper(
×
185
        file_name=output_path.name,
186
        module_name=module_name,
187
        len_buf=len_buf,
188
        constants=constants,
189
        local_derived_types=local_derived_types,
190
        kind_module=kind_module,
191
        kind_map=kind_map,
192
        kind_allowlist=kind_allowlist,
193
        module_doc=module_doc,
194
        helper_header=helper_header,
195
    )
196
    output_path.parent.mkdir(parents=True, exist_ok=True)
×
197
    output_path.write_text(rendered, encoding="ascii")
×
198

199

200
def render_helper(
1✔
201
    *,
202
    file_name: str,
203
    module_name: str = "nml_helper",
204
    len_buf: int = 1024,
205
    constants: list[ConstantSpec] | None = None,
206
    local_derived_types: list[LocalDerivedTypeSpec] | None = None,
207
    kind_module: str | None = None,
208
    kind_map: dict[str, str] | None = None,
209
    kind_allowlist: Iterable[str] | None = None,
210
    module_doc: str | None = None,
211
    helper_header: str | None = None,
212
) -> str:
213
    """Render the helper Fortran module."""
214
    if not module_name:
1✔
215
        raise ValueError("helper module name must be a non-empty string")
×
216
    if len_buf <= 0:
1✔
217
        raise ValueError("helper len_buf must be positive")
×
218
    local_types = local_derived_types or []
1✔
219
    helper_kind_ids = [
1✔
220
        kind_id for type_spec in local_types for kind_id in type_spec.kind_ids
221
    ]
222
    return _TEMPLATE_ENV.get_template("nml_helper.f90.j2").render(
1✔
223
        {
224
            "file_name": file_name,
225
            "module_name": module_name,
226
            "len_buf": len_buf,
227
            "constants": constants or [],
228
            "local_derived_types": local_types,
229
            "kind_module": kind_module or "iso_fortran_env",
230
            "kind_imports": _resolve_kind_imports(
231
                helper_kind_ids,
232
                kind_map=kind_map,
233
                kind_allowlist=kind_allowlist,
234
            ),
235
            "module_doc": module_doc,
236
            "helper_header": helper_header,
237
        }
238
    )
239

240

241
def collect_local_derived_types(
1✔
242
    schemas: Iterable[dict[str, Any]],
243
    *,
244
    constants: dict[str, int] | None = None,
245
) -> list[LocalDerivedTypeSpec]:
246
    """Collect locally owned derived definitions used by namelist schemas."""
247
    static_constants = normalize_constant_values(constants)
1✔
248
    collected: dict[tuple[str, str], LocalDerivedTypeSpec] = {}
1✔
249
    type_owners: dict[str, tuple[str, str]] = {}
1✔
250
    for schema in schemas:
1✔
251
        properties = schema.get("properties")
1✔
252
        if not isinstance(properties, dict):
1✔
NEW
253
            continue
×
254
        for prop in properties.values():
1✔
255
            if not isinstance(prop, dict):
1✔
NEW
256
                continue
×
257
            derived = _derived_schema(prop)
1✔
258
            if derived is None or derived.get("x-fortran-module") is not None:
1✔
259
                continue
1✔
260
            origin = _derived_origin(derived)
1✔
261
            identity = origin["identity"]
1✔
262
            definition = origin["definition"]
1✔
263
            type_name = _derived_type_name(definition)
1✔
264
            owner = type_owners.get(type_name.lower())
1✔
265
            if owner is not None and owner != identity:
1✔
266
                raise ValueError(
1✔
267
                    f"local derived type name '{type_name}' is used by distinct definitions"
268
                )
269
            type_owners[type_name.lower()] = identity
1✔
270
            if identity in collected:
1✔
271
                continue
1✔
272
            component_properties = definition.get("properties")
1✔
273
            if not isinstance(component_properties, dict):
1✔
NEW
274
                raise ValueError(f"derived type '{type_name}' must define properties")
×
275
            declarations: list[str] = []
1✔
276
            kind_ids: list[str] = []
1✔
277
            for name, component in component_properties.items():
1✔
278
                if not isinstance(name, str) or not isinstance(component, dict):
1✔
NEW
279
                    raise ValueError(f"derived type '{type_name}' has invalid components")
×
280
                info = _scalar_type_info(component, static_constants)
1✔
281
                if info.kind is not None:
1✔
282
                    kind_ids.append(info.kind)
1✔
283
                title = component.get("title", name)
1✔
284
                declarations.append(f"{info.type_spec} :: {name} !< {title}")
1✔
285
            title = definition.get("title", type_name)
1✔
286
            description = definition.get("description")
1✔
287
            collected[identity] = LocalDerivedTypeSpec(
1✔
288
                identity=identity,
289
                type_name=type_name,
290
                title=str(title),
291
                description=str(description) if description is not None else None,
292
                declarations=declarations,
293
                kind_ids=kind_ids,
294
            )
295
    return list(collected.values())
1✔
296

297

298
def _build_context(
1✔
299
    schema: dict[str, Any],
300
    *,
301
    helper_module: str,
302
    kind_module: str | None,
303
    kind_map: dict[str, str] | None,
304
    kind_allowlist: Iterable[str] | None,
305
    constants: dict[str, int] | None,
306
    dimensions: dict[str, int] | None = None,
307
    module_doc: str | None = None,
308
    f2py_handle_helpers: bool = False,
309
) -> dict[str, Any]:
310
    if not helper_module:
1✔
311
        raise ValueError("helper module name must be a non-empty string")
×
312
    if "x-fortran-kind-module" in schema:
1✔
313
        raise ValueError("schema must not define 'x-fortran-kind-module'")
×
314
    namelist_name = schema.get("x-fortran-namelist")
1✔
315
    if not isinstance(namelist_name, str):
1✔
316
        raise ValueError("schema must define 'x-fortran-namelist'")
×
317

318
    if schema.get("type") != "object":
1✔
319
        raise ValueError("schema root must be of type 'object'")
×
320

321
    properties = schema.get("properties")
1✔
322
    if not isinstance(properties, dict) or not properties:
1✔
323
        raise ValueError("schema must define object 'properties'")
×
324

325
    property_items: list[tuple[str, str, dict[str, Any]]] = []
1✔
326
    property_name_map: dict[str, str] = {}
1✔
327
    for prop_name, prop in properties.items():
1✔
328
        if not isinstance(prop_name, str) or not prop_name.strip():
1✔
329
            raise ValueError("property names must be non-empty strings")
×
330
        key = prop_name.lower()
1✔
331
        if key in property_name_map:
1✔
332
            raise ValueError(
1✔
333
                "property names must be unique (case-insensitive): "
334
                f"'{property_name_map[key]}' and '{prop_name}'"
335
            )
336
        property_name_map[key] = prop_name
1✔
337
        if not isinstance(prop, dict):
1✔
338
            raise ValueError(f"property '{prop_name}' must be an object")
×
339
        property_items.append((prop_name, key, prop))
1✔
340

341
    required_fields_raw = _ordered_unique(schema.get("required", []))
1✔
342
    required_fields: list[str] = []
1✔
343
    for req_name in required_fields_raw:
1✔
344
        if not isinstance(req_name, str):
1✔
345
            raise ValueError("schema 'required' entries must be strings")
×
346
        req_key = req_name.lower()
1✔
347
        if req_key not in property_name_map:
1✔
348
            raise ValueError(f"required property '{req_name}' is not defined")
1✔
349
        if req_key not in required_fields:
1✔
350
            required_fields.append(req_key)
1✔
351
    required_set = set(required_fields)
1✔
352
    reserved_property_default_names: set[str] = set()
1✔
353
    for _, attr_name, prop in property_items:
1✔
354
        has_default_parameter = False
1✔
355
        if prop.get("type") == "array":
1✔
356
            has_default_parameter = _array_default_value(prop) is not None
1✔
357
        else:
358
            has_default_parameter = "default" in prop
1✔
359
        if has_default_parameter:
1✔
360
            reserved_property_default_names.add(f"{attr_name}_default".lower())
1✔
361

362
    module_name = f"nml_{namelist_name}"
1✔
363
    type_name = f"{module_name}_t"
1✔
364
    doc_class = f"{module_name}_t"
1✔
365
    brief_text = schema.get("title", namelist_name)
1✔
366
    details_text = schema.get("description", brief_text)
1✔
367

368
    fields: list[FieldSpec] = []
1✔
369
    sentinel_assignments: list[str] = []
1✔
370
    default_assignments: list[str] = []
1✔
371
    set_optional_defaults: list[str] = []
1✔
372
    local_init_assignments: list[str] = []
1✔
373
    set_required_assignments: list[str] = []
1✔
374
    presence_cases: list[dict[str, Any]] = []
1✔
375
    required_scalar_names: set[str] = set()
1✔
376
    required_array_by_name: dict[str, dict[str, Any]] = {}
1✔
377
    flex_bound_vars: set[str] = set()
1✔
378
    flex_arrays: list[dict[str, Any]] = []
1✔
379
    default_parameters: list[str] = []
1✔
380
    enum_parameters: list[str] = []
1✔
381
    enum_functions: list[dict[str, Any]] = []
1✔
382
    enum_checks: list[dict[str, Any]] = []
1✔
383
    bounds_parameters: list[str] = []
1✔
384
    bounds_functions: list[dict[str, Any]] = []
1✔
385
    bounds_checks: list[dict[str, Any]] = []
1✔
386
    derived_type_imports: list[dict[str, str]] = []
1✔
387
    derived_init_type_fields: list[dict[str, Any]] = []
1✔
388
    derived_presence_blocks: list[str] = []
1✔
389
    derived_post_assignment_checks: list[str] = []
1✔
390
    static_constants = normalize_constant_values(constants)
1✔
391
    runtime_dimension_values = normalize_runtime_dimensions(dimensions)
1✔
392
    reject_constant_dimension_overlap(static_constants, runtime_dimension_values)
1✔
393
    validate_schema_defaults(
1✔
394
        schema,
395
        constants=static_constants,
396
        dimensions=runtime_dimension_values,
397
    )
398
    shape_constants: dict[str, int] = {**static_constants, **runtime_dimension_values}
1✔
399
    runtime_dimensions: list[dict[str, str]] = []
1✔
400
    runtime_dimension_locals: dict[str, str] = {}
1✔
401
    runtime_default_extent_requirements: list[dict[str, Any]] = []
1✔
402
    runtime_allocations: list[str] = []
1✔
403
    runtime_deallocations: list[str] = []
1✔
404
    runtime_local_allocations: list[str] = []
1✔
405
    kind_ids: list[str] = []
1✔
406
    requires_ieee = False
1✔
407
    uses_partly_set = False
1✔
408
    helper_imports = [
1✔
409
        "nml_file_t",
410
        "nml_line_buffer",
411
        "NML_OK",
412
        "NML_ERR_FILE_NOT_FOUND",
413
        "NML_ERR_OPEN",
414
        "NML_ERR_NOT_OPEN",
415
        "NML_ERR_NML_NOT_FOUND",
416
        "NML_ERR_READ",
417
        "NML_ERR_CLOSE",
418
        "NML_ERR_REQUIRED",
419
        "NML_ERR_ENUM",
420
        "NML_ERR_BOUNDS",
421
        "NML_ERR_NOT_SET",
422
        "NML_ERR_INVALID_NAME",
423
        "NML_ERR_INVALID_INDEX",
424
        "idx_check",
425
        "to_lower",
426
    ]
427
    if f2py_handle_helpers:
1✔
428
        helper_imports.append("NML_ERR_INVALID_HANDLE")
1✔
429

430
    def _helper_import_local_name(import_spec: str) -> str:
1✔
431
        if "=>" in import_spec:
1✔
432
            return import_spec.split("=>", 1)[0].strip()
×
433
        return import_spec.strip()
1✔
434

435
    def _helper_import_local_names() -> set[str]:
1✔
436
        return {_helper_import_local_name(existing).lower() for existing in helper_imports}
1✔
437

438
    def _add_helper_import(name: str) -> None:
1✔
439
        if not any(existing.lower() == name.lower() for existing in helper_imports):
1✔
440
            helper_imports.append(name)
1✔
441

442
    def _unique_generated_name(base_name: str, taken_names: set[str]) -> str:
1✔
443
        if base_name.lower() not in taken_names:
1✔
444
            return base_name
1✔
445
        index = 1
1✔
446
        while True:
1✔
447
            candidate = f"{base_name}_{index}"
1✔
448
            if candidate.lower() not in taken_names:
1✔
449
                return candidate
1✔
450
            index += 1
×
451

452
    def _unique_helper_import_alias(base_name: str) -> str:
1✔
453
        local_names = (
1✔
454
            _helper_import_local_names() | set(static_constants) | reserved_property_default_names
455
        )
456
        return _unique_generated_name(base_name, local_names)
1✔
457

458
    def _register_runtime_dimension(dim_name: str) -> str:
1✔
459
        local_name = runtime_dimension_locals.get(dim_name)
1✔
460
        if local_name is None:
1✔
461
            local_base = f"dim_{dim_name}"
1✔
462
            local_taken = set(property_name_map) | {
1✔
463
                existing.lower() for existing in runtime_dimension_locals.values()
464
            }
465
            local_name = _unique_generated_name(local_base, local_taken)
1✔
466
            runtime_dimension_locals[dim_name] = local_name
1✔
467
            default_name = _unique_helper_import_alias(f"{dim_name}_default")
1✔
468
            _add_helper_import(f"{default_name}=>{dim_name}")
1✔
469
            runtime_dimensions.append(
1✔
470
                {
471
                    "name": dim_name,
472
                    "default_name": default_name,
473
                    "local_name": local_name,
474
                }
475
            )
476
        return local_name
1✔
477

478
    current_property: str | None = None
1✔
479
    try:
1✔
480
        for index, (display_name, attr_name, prop) in enumerate(property_items):
1✔
481
            current_property = display_name
1✔
482
            name = attr_name
1✔
483
            _reject_runtime_dimension_lengths(prop, runtime_dimension_values)
1✔
484
            type_info = _field_type_info(prop, static_constants)
1✔
485
            for const_name in _collect_dimension_constants(type_info.dimensions, shape_constants):
1✔
486
                const_key = const_name.lower()
1✔
487
                if const_key in runtime_dimension_values:
1✔
488
                    _register_runtime_dimension(const_key)
1✔
489
                else:
490
                    _add_helper_import(const_name)
1✔
491
            if type_info.length_expr and not _is_int_literal(type_info.length_expr):
1✔
492
                _add_helper_import(type_info.length_expr)
1✔
493
            if type_info.kind:
1✔
494
                kind_ids.append(type_info.kind)
1✔
495

496
            runtime_shape = list(type_info.dimensions)
1✔
497
            dynamic_shape = False
1✔
498
            if type_info.category == "array":
1✔
499
                for dim_index, dim in enumerate(runtime_shape):
1✔
500
                    if dim == ":" or _is_int_literal(dim):
1✔
501
                        continue
1✔
502
                    if not FORTRAN_IDENTIFIER.match(dim):
1✔
503
                        raise ValueError(
×
504
                            "array property 'x-fortran-shape' entries must be ints or identifiers"
505
                        )
506
                    dim_key = dim.lower()
1✔
507
                    if dim_key in runtime_dimension_values:
1✔
508
                        local_dim_name = _register_runtime_dimension(dim_key)
1✔
509
                        runtime_shape[dim_index] = f"this%{local_dim_name}"
1✔
510
                        dynamic_shape = True
1✔
511
                    elif dim_key not in static_constants:
1✔
512
                        raise ValueError(f"dimension constant '{dim}' is not defined in config")
×
513

514
            runtime_length_expr = type_info.length_expr
1✔
515
            if (
1✔
516
                type_info.length_expr
517
                and not _is_int_literal(type_info.length_expr)
518
                and type_info.length_expr.lower() in runtime_dimension_values
519
            ):
520
                raise ValueError(
×
521
                    f"dimension '{type_info.length_expr}' cannot be used as x-fortran-len"
522
                )
523
            type_spec_with_defaults = type_info.type_spec
1✔
524

525
            array_default_info: tuple[Any, bool] | None = None
1✔
526
            if type_info.category == "array":
1✔
527
                array_default_info = _array_default_value(prop)
1✔
528

529
            flex_dim = _parse_flex_dim(prop, type_info)
1✔
530
            if flex_dim > 0:
1✔
531
                if type_info.element_category == "boolean":
1✔
532
                    raise ValueError("flex arrays cannot use boolean elements")
1✔
533
                if array_default_info is not None or any(
1✔
534
                    key in prop
535
                    for key in (
536
                        "x-fortran-default-order",
537
                        "x-fortran-default-repeat",
538
                        "x-fortran-default-pad",
539
                    )
540
                ):
541
                    raise ValueError("flex arrays cannot define defaults")
×
542

543
            is_runtime_sized = (
1✔
544
                type_info.category == "array" and dynamic_shape
545
            )
546

547
            if is_runtime_sized:
1✔
548
                declaration = _render_runtime_declaration(
1✔
549
                    type_info,
550
                    name,
551
                )
552
                local_decl = _render_runtime_local_declaration(
1✔
553
                    type_info,
554
                    name,
555
                    runtime_dimensions=runtime_shape,
556
                )
557
                runtime_allocations.extend(
1✔
558
                    _render_runtime_allocations(
559
                        type_info,
560
                        name,
561
                        runtime_dimensions=runtime_shape,
562
                        runtime_length_expr=runtime_length_expr,
563
                        target_prefix="this%",
564
                    )
565
                )
566
                runtime_deallocations.extend(
1✔
567
                    _render_runtime_deallocations(name, target_prefix="this%")
568
                )
569
                runtime_local_allocations.extend(
1✔
570
                    _render_runtime_local_allocations(
571
                        type_info,
572
                        name,
573
                        runtime_dimensions=runtime_shape,
574
                        runtime_length_expr=runtime_length_expr,
575
                    )
576
                )
577
            else:
578
                declaration = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
579
                local_decl = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
580

581
            title_raw = prop.get("title")
1✔
582
            if title_raw is None:
1✔
583
                title = display_name
1✔
584
            elif not isinstance(title_raw, str):
1✔
585
                raise ValueError(f"property '{display_name}' title must be a string")
×
586
            else:
587
                title = title_raw.strip() or name
1✔
588
            description = prop.get("description")
1✔
589
            declaration_with_doc = f"{declaration} !< {title}"
1✔
590

591
            dynamic_array = type_info.category == "array" and is_runtime_sized
1✔
592

593
            is_required = name in required_set
1✔
594
            derived = _derived_schema(prop)
1✔
595
            if derived is not None:
1✔
596
                derived_type_name = _derived_type_name(derived)
1✔
597
                module = derived.get("x-fortran-module")
1✔
598
                if module is None:
1✔
599
                    _add_helper_import(derived_type_name)
1✔
600
                elif not any(
1✔
601
                    entry["module"].lower() == str(module).lower()
602
                    and entry["type_name"].lower() == derived_type_name.lower()
603
                    for entry in derived_type_imports
604
                ):
605
                    derived_type_imports.append(
1✔
606
                        {"module": str(module), "type_name": derived_type_name}
607
                    )
608

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

823
                arg_dimensions = [":" for _ in type_info.dimensions]
1✔
824
                argument_decl = _render_argument_declaration(
1✔
825
                    name=name,
826
                    type_info=type_info,
827
                    is_required=is_required,
828
                    dimensions=arg_dimensions,
829
                    doc=title,
830
                )
831
                local_init_assignments.append(f"{name} = this%{name}")
1✔
832
                derived_partial_bounds: list[dict[str, Any]] = []
1✔
833
                set_present_assignment: str | None = None
1✔
834
                if type_info.category == "array":
1✔
835
                    for array_dim_index in range(1, len(type_info.dimensions) + 1):
1✔
836
                        lb_var, ub_var = _flex_bound_vars(array_dim_index)
1✔
837
                        derived_partial_bounds.append(
1✔
838
                            {"dim": array_dim_index, "lb_var": lb_var, "ub_var": ub_var}
839
                        )
840
                        flex_bound_vars.add(lb_var)
1✔
841
                        flex_bound_vars.add(ub_var)
1✔
842
                if is_required:
1✔
843
                    if type_info.category == "array":
1✔
844
                        set_required_assignments.append(
1✔
845
                            _render_partial_set_block(
846
                                name, len(type_info.dimensions), derived_partial_bounds
847
                            )
848
                        )
849
                    else:
850
                        set_required_assignments.append(f"this%{name} = {name}")
1✔
851
                elif type_info.category == "array":
1✔
NEW
852
                    block = _render_partial_set_block(
×
853
                        name, len(type_info.dimensions), derived_partial_bounds
854
                    )
NEW
855
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
×
NEW
856
                    set_present_assignment = (
×
857
                        f"if (present({name})) then\n{indented_block}\nend if"
858
                    )
859
                else:
860
                    set_present_assignment = f"if (present({name})) this%{name} = {name}"
1✔
861

862
                init_argument_declaration = _render_argument_declaration(
1✔
863
                    name=name,
864
                    type_info=type_info,
865
                    is_required=False,
866
                    dimensions=arg_dimensions,
867
                    doc=title,
868
                )
869
                allocation_lines: list[str] = []
1✔
870
                if type_info.category == "array":
1✔
871
                    init_argument_declaration = init_argument_declaration.replace(
1✔
872
                        ", intent(in), optional", ", allocatable, intent(inout), optional"
873
                    )
874
                    allocation_lines.append(f"if (allocated({name})) deallocate({name})")
1✔
875
                    dims = ", ".join(runtime_shape)
1✔
876
                    allocation_lines.append(f"allocate({name}({dims}))")
1✔
877
                else:
878
                    init_argument_declaration = init_argument_declaration.replace(
1✔
879
                        "intent(in)", "intent(inout)"
880
                    )
881
                derived_init_type_fields.append(
1✔
882
                    {
883
                        "name": name,
884
                        "declaration": init_argument_declaration,
885
                        "allocation_lines": allocation_lines,
886
                        "init_lines": init_lines,
887
                    }
888
                )
889
                derived_presence_blocks.extend(
1✔
890
                    _derived_presence_cases(
891
                        name=name,
892
                        display_name=display_name,
893
                        leaves=leaf_entries,
894
                        is_array=type_info.category == "array",
895
                        runtime_array=dynamic_array,
896
                        rank=len(type_info.dimensions),
897
                    )
898
                )
899
                if is_required:
1✔
900
                    uses_partly_set = uses_partly_set or (
1✔
901
                        len(inner_required) > 1
902
                        or (type_info.category == "array" and bool(inner_required))
903
                    )
904
                fields.append(
1✔
905
                    FieldSpec(
906
                        order=index,
907
                        name=name,
908
                        title=title,
909
                        description=description,
910
                        declaration=f"{declaration} !< {title}",
911
                        local_declaration=local_decl,
912
                        required=is_required,
913
                        sentinel_assignment=None,
914
                        sentinel_check=None,
915
                        default_assignment=None,
916
                        set_default_assignment=None,
917
                        set_present_assignment=set_present_assignment,
918
                        argument_declaration=argument_decl,
919
                        type_category=type_info.category,
920
                        runtime_sized_array=dynamic_array,
921
                        rank=len(type_info.dimensions),
922
                    )
923
                )
924
                continue
1✔
925
            if type_info.category == "array":
1✔
926
                has_default = array_default_info is not None
1✔
927
            else:
928
                has_default = "default" in prop
1✔
929

930
            default_from_items = False
1✔
931
            default_values: list[Any] | None = None
1✔
932
            parsed_dims: list[int] | None = None
1✔
933
            array_default_spec: ArrayDefaultSpec | None = None
1✔
934

935
            if type_info.category == "array" and has_default:
1✔
936
                if array_default_info is None:
1✔
937
                    raise ValueError(f"missing array default for '{display_name}'")
×
938
                default_raw, default_from_items = array_default_info
1✔
939
                if default_from_items:
1✔
940
                    if isinstance(default_raw, list):
1✔
941
                        raise ValueError("array items default must be a scalar")
×
942
                    default_values = [default_raw]
1✔
943
                else:
944
                    if not isinstance(default_raw, list):
1✔
945
                        raise ValueError("array default must be a list")
×
946
                    default_values = default_raw
1✔
947
                    parsed_dims = _parse_default_dimensions(type_info.dimensions, shape_constants)
1✔
948
                    array_default_spec = _prepare_array_default(default_values, parsed_dims, prop)
1✔
949

950
            needs_sentinel = (not is_required) and (not has_default)
1✔
951
            requires_sentinel = is_required or needs_sentinel
1✔
952

953
            arg_dimensions = type_info.dimensions
1✔
954
            if type_info.category == "array":
1✔
955
                arg_dimensions = [":" for _ in type_info.dimensions]
1✔
956
            argument_decl = _render_argument_declaration(
1✔
957
                name=name,
958
                type_info=type_info,
959
                is_required=is_required,
960
                dimensions=arg_dimensions,
961
                doc=title,
962
            )
963
            local_init_assignments.append(f"{name} = this%{name}")
1✔
964

965
            sentinel_assignment: str | None = None
1✔
966
            sentinel_condition: str | None = None
1✔
967
            set_sentinel_condition: str | None = None
1✔
968
            if requires_sentinel:
1✔
969
                if is_required and type_info.category == "boolean":
1✔
970
                    raise ValueError(
×
971
                        f"required {type_info.category} '{display_name}' is not supported"
972
                    )
973
                if is_required and type_info.category == "array":
1✔
974
                    if type_info.element_category == "boolean":
1✔
975
                        raise ValueError("required boolean arrays are not supported")
×
976
                if needs_sentinel and type_info.category == "boolean":
1✔
977
                    raise ValueError(f"optional boolean '{display_name}' must define a default")
×
978
                if needs_sentinel and type_info.category == "array":
1✔
979
                    if type_info.element_category == "boolean":
1✔
980
                        raise ValueError("optional boolean arrays must define a default")
×
981
                value_expr, condition_expr, uses_ieee = _sentinel_expressions(
1✔
982
                    type_info,
983
                    var_ref=f"this%{name}",
984
                )
985
                sentinel_assignment = _render_sentinel_assignment(
1✔
986
                    type_info,
987
                    target_ref=f"this%{name}",
988
                    value_expr=value_expr,
989
                    comment=_sentinel_comment(type_info, required=is_required),
990
                )
991
                sentinel_condition = condition_expr
1✔
992
                sentinel_assignments.append(sentinel_assignment)
1✔
993
                if uses_ieee:
1✔
994
                    requires_ieee = True
1✔
995
                if not has_default and type_info.category != "boolean":
1✔
996
                    set_value_expr, set_condition_expr, set_uses_ieee = _sentinel_expressions(
1✔
997
                        type_info,
998
                        var_ref=f"this%{name}",
999
                    )
1000
                    if set_uses_ieee:
1✔
1001
                        requires_ieee = True
1✔
1002
                    set_sentinel_condition = set_condition_expr
1✔
1003
                    if needs_sentinel:
1✔
1004
                        sent_com = _sentinel_comment(type_info, required=False)
1✔
1005
                        set_optional_defaults.append(
1✔
1006
                            _render_sentinel_assignment(
1007
                                type_info,
1008
                                target_ref=f"this%{name}",
1009
                                value_expr=set_value_expr,
1010
                                comment=sent_com,
1011
                            )
1012
                        )
1013

1014
            if is_required and type_info.category != "array":
1✔
1015
                required_scalar_names.add(name)
1✔
1016

1017
            if is_required and type_info.category == "array" and flex_dim == 0:
1✔
1018
                element_category = type_info.element_category
1✔
1019
                if element_category is None:
1✔
1020
                    raise ValueError("array field missing element category")
×
1021
                all_missing, any_missing, uses_ieee = _array_missing_conditions(
1✔
1022
                    element_category,
1023
                    var_ref=_array_section_ref(f"this%{name}", len(type_info.dimensions)),
1024
                    len_ref=f"this%{name}",
1025
                )
1026
                required_array_by_name[name] = {
1✔
1027
                    "name": display_name,
1028
                    "attr_name": name,
1029
                    "runtime_array": dynamic_array,
1030
                    "all_missing_condition": all_missing,
1031
                    "any_missing_condition": any_missing,
1032
                }
1033
                uses_partly_set = True
1✔
1034
                if uses_ieee:
1✔
1035
                    requires_ieee = True
1✔
1036

1037
            partial_bounds: list[dict[str, Any]] | None = None
1✔
1038
            if type_info.category == "array":
1✔
1039
                rank = len(type_info.dimensions)
1✔
1040
                all_bounds = []
1✔
1041
                for array_dim_index in range(1, rank + 1):
1✔
1042
                    lb_var, ub_var = _flex_bound_vars(array_dim_index)
1✔
1043
                    all_bounds.append(
1✔
1044
                        {"dim": array_dim_index, "lb_var": lb_var, "ub_var": ub_var}
1045
                    )
1046
                    flex_bound_vars.add(lb_var)
1✔
1047
                    flex_bound_vars.add(ub_var)
1✔
1048
                partial_bounds = all_bounds
1✔
1049
            if flex_dim > 0:
1✔
1050
                rank = len(type_info.dimensions)
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
                flex_dims: list[int] = list(range(rank - flex_dim + 1, rank + 1))
1✔
1055
                slice_missing_conditions: list[str] = []
1✔
1056
                slice_uses_ieee = False
1✔
1057
                flex_dim_bounds: list[dict[str, Any]] = []
1✔
1058
                lb_vars: dict[int, str] = {}
1✔
1059
                ub_vars: dict[int, str] = {}
1✔
1060
                for flex_dim_index in flex_dims:
1✔
1061
                    lb_var, ub_var = _flex_bound_vars(flex_dim_index)
1✔
1062
                    lb_vars[flex_dim_index] = lb_var
1✔
1063
                    ub_vars[flex_dim_index] = ub_var
1✔
1064
                    flex_dim_bounds.append(
1✔
1065
                        {"dim": flex_dim_index, "lb_var": lb_var, "ub_var": ub_var}
1066
                    )
1067
                    flex_bound_vars.add(lb_var)
1✔
1068
                    flex_bound_vars.add(ub_var)
1✔
1069
                    slice_ref = _slice_ref(name, rank, flex_dim_index, "idx")
1✔
1070
                    slice_missing_expr, uses_ieee = _element_missing_expression(
1✔
1071
                        element_category,
1072
                        var_ref=slice_ref,
1073
                        len_ref=f"this%{name}",
1074
                    )
1075
                    slice_missing_conditions.append(
1✔
1076
                        f"all({slice_missing_expr})" if rank > 1 else slice_missing_expr
1077
                    )
1078
                    slice_uses_ieee = slice_uses_ieee or uses_ieee
1✔
1079
                prefix_ref = _slice_ref_bounds(name, rank, flex_dims, lb_vars, ub_vars)
1✔
1080
                prefix_missing_expr, uses_ieee_prefix = _element_missing_expression(
1✔
1081
                    element_category,
1082
                    var_ref=prefix_ref,
1083
                    len_ref=f"this%{name}",
1084
                )
1085
                prefix_any_missing_condition = f"any({prefix_missing_expr})"
1✔
1086
                flex_arrays.append(
1✔
1087
                    {
1088
                        "name": name,
1089
                        "display_name": display_name,
1090
                        "rank": rank,
1091
                        "flex_dims": flex_dims,
1092
                        "required": is_required,
1093
                        "runtime_array": dynamic_array,
1094
                        "bounds": flex_dim_bounds,
1095
                        "slice_missing_conditions": slice_missing_conditions,
1096
                        "prefix_any_missing_condition": prefix_any_missing_condition,
1097
                    }
1098
                )
1099
                uses_partly_set = True
1✔
1100
                if slice_uses_ieee or uses_ieee_prefix:
1✔
1101
                    requires_ieee = True
×
1102

1103
            default_assignment: str | None = None
1✔
1104
            set_default_assignment: str | None = None
1✔
1105
            if has_default and is_required:
1✔
1106
                raise ValueError(f"required property '{display_name}' cannot define a default")
×
1107
            if has_default:
1✔
1108
                default_const_name = f"{name}_default"
1✔
1109
                if type_info.category == "array":
1✔
1110
                    if default_values is None:
1✔
1111
                        raise ValueError(f"missing array default for '{display_name}'")
×
1112
                    default_is_scalar = default_from_items
1✔
1113

1114
                    repeat = False
1✔
1115
                    pad_raw = None
1✔
1116
                    pad_const_name: str | None = None
1✔
1117
                    pad_is_scalar = False
1✔
1118

1119
                    if not default_from_items:
1✔
1120
                        repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
1121
                        if not isinstance(repeat_raw, bool):
1✔
1122
                            raise ValueError("array default repeat must be a boolean")
×
1123
                        repeat = bool(repeat_raw)
1✔
1124
                        pad_raw = prop.get("x-fortran-default-pad")
1✔
1125
                        if pad_raw is not None:
1✔
1126
                            pad_const_name = f"{name}_pad"
1✔
1127
                            pad_is_scalar = not isinstance(pad_raw, list)
1✔
1128
                            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
1✔
1129
                            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
1✔
1130
                            pad_elements = [
1✔
1131
                                _format_scalar_default(
1132
                                    element, type_info.kind, type_info.element_category
1133
                                )
1134
                                for element in pad_values
1135
                            ]
1136
                            if pad_is_scalar:
1✔
1137
                                pad_literal = _format_scalar_default(
1✔
1138
                                    pad_raw, type_info.kind, type_info.element_category
1139
                                )
1140
                                default_parameters.append(
1✔
1141
                                    f"{type_spec_with_defaults}, parameter, public :: "
1142
                                    f"{pad_const_name} = {pad_literal}"
1143
                                )
1144
                            else:
1145
                                default_parameters.append(
×
1146
                                    f"{type_spec_with_defaults}, parameter, public :: "
1147
                                    f"{pad_const_name}({len(pad_elements)}) = "
1148
                                    f"[{', '.join(pad_elements)}]"
1149
                                )
1150

1151
                        if parsed_dims is None:
1✔
1152
                            parsed_dims = _parse_default_dimensions(
×
1153
                                type_info.dimensions, shape_constants
1154
                            )
1155
                        if array_default_spec is None:
1✔
1156
                            array_default_spec = _prepare_array_default(
×
1157
                                default_values, parsed_dims, prop
1158
                            )
1159

1160
                    if default_is_scalar:
1✔
1161
                        default_literal = _format_scalar_default(
1✔
1162
                            default_values[0], type_info.kind, type_info.element_category
1163
                        )
1164
                        default_parameters.append(
1✔
1165
                            f"{type_spec_with_defaults}, parameter, public :: "
1166
                            f"{default_const_name} = {default_literal}"
1167
                        )
1168
                    else:
1169
                        if array_default_spec is None:
1✔
1170
                            raise ValueError(
×
1171
                                f"missing array default specification for '{display_name}'"
1172
                            )
1173
                        default_elements = [
1✔
1174
                            _format_scalar_default(
1175
                                element, type_info.kind, type_info.element_category
1176
                            )
1177
                            for element in array_default_spec.source_values
1178
                        ]
1179
                        default_parameters.append(
1✔
1180
                            f"{type_spec_with_defaults}, parameter, public :: "
1181
                            f"{default_const_name}({len(default_elements)}) = "
1182
                            f"[{', '.join(default_elements)}]"
1183
                        )
1184

1185
                    if default_from_items:
1✔
1186
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
1187
                    elif (
1✔
1188
                        len(type_info.dimensions) == 1
1189
                        and array_default_spec is not None
1190
                        and array_default_spec.order_values is None
1191
                        and array_default_spec.pad_values is None
1192
                    ):
1193
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
1194
                    else:
1195
                        source_expr = default_const_name
1✔
1196
                        shape_expr = ", ".join(runtime_shape)
1✔
1197
                        arguments = [source_expr, f"shape=[{shape_expr}]"]
1✔
1198
                        if array_default_spec is not None:
1✔
1199
                            if array_default_spec.order_values is not None:
1✔
1200
                                order_literal = ", ".join(
1✔
1201
                                    str(index) for index in array_default_spec.order_values
1202
                                )
1203
                                arguments.append(f"order=[{order_literal}]")
1✔
1204
                            if array_default_spec.pad_values is not None:
1✔
1205
                                if repeat:
1✔
1206
                                    pad_expr = default_const_name
1✔
1207
                                else:
1208
                                    if pad_const_name is None:
1✔
1209
                                        raise ValueError(
×
1210
                                            f"missing pad values for array default '{display_name}'"
1211
                                        )
1212
                                    pad_expr = (
1✔
1213
                                        pad_const_name
1214
                                        if not pad_is_scalar
1215
                                        else f"[{pad_const_name}]"
1216
                                    )
1217
                                arguments.append(f"pad={pad_expr}")
1✔
1218
                        default_assignment = _format_reshape_assignment(name, arguments)
1✔
1219
                    set_default_assignment = default_assignment
1✔
1220
                else:
1221
                    default_literal = _format_default(prop["default"], type_info, prop, constants)
1✔
1222
                    default_parameters.append(
1✔
1223
                        f"{type_spec_with_defaults}, parameter, public :: "
1224
                        f"{default_const_name} = {default_literal}"
1225
                    )
1226
                    if type_info.category == "boolean":
1✔
1227
                        default_assignment = (
1✔
1228
                            f"this%{name} = {default_const_name} "
1229
                            "! bool values always need a default"
1230
                        )
1231
                    else:
1232
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
1233
                    set_default_assignment = f"this%{name} = {default_const_name}"
1✔
1234

1235
                if default_assignment is None or set_default_assignment is None:
1✔
1236
                    raise ValueError(f"missing default assignment for '{display_name}'")
×
1237
                default_assignments.append(default_assignment)
1✔
1238
                set_optional_defaults.append(set_default_assignment)
1✔
1239

1240
            if type_info.category == "array" and any(
1✔
1241
                (dim != ":") and (not _is_int_literal(dim)) for dim in type_info.dimensions
1242
            ):
1243
                required_default_elements: int | None = None
1✔
1244
                if has_default and default_values is not None:
1✔
1245
                    if default_from_items:
1✔
1246
                        required_default_elements = 1
1✔
1247
                    elif array_default_spec is not None:
1✔
1248
                        required_default_elements = len(array_default_spec.source_values)
1✔
1249
                    else:
1250
                        required_default_elements = len(default_values)
×
1251
                if required_default_elements is not None and required_default_elements > 1:
1✔
1252
                    runtime_default_extent_requirements.append(
1✔
1253
                        {
1254
                            "field": display_name,
1255
                            "dimensions": list(type_info.dimensions),
1256
                            "required_elements": required_default_elements,
1257
                        }
1258
                    )
1259

1260
            enum_values = _enum_values(prop, type_info, constants)
1✔
1261
            if enum_values is not None:
1✔
1262
                enum_category = _enum_category(type_info)
1✔
1263
                enum_const_name = f"{name}_enum_values"
1✔
1264
                enum_literals = [
1✔
1265
                    _format_scalar_default(value, type_info.kind, enum_category)
1266
                    for value in enum_values
1267
                ]
1268
                if enum_category == "string":
1✔
1269
                    enum_array_literal = (
1✔
1270
                        f"[{type_spec_with_defaults} :: {', '.join(enum_literals)}]"
1271
                    )
1272
                else:
1273
                    enum_array_literal = f"[{', '.join(enum_literals)}]"
1✔
1274
                if enum_category == "string":
1✔
1275
                    enum_parameters.append(
1✔
1276
                        f"{type_spec_with_defaults}, parameter, public :: &\n"
1277
                        f"    {enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
1278
                    )
1279
                else:
1280
                    enum_parameters.append(
1✔
1281
                        f"{type_spec_with_defaults}, parameter, public :: "
1282
                        f"{enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
1283
                    )
1284
                enum_type_info = (
1✔
1285
                    _element_type_info(type_info) if type_info.category == "array" else type_info
1286
                )
1287
                _, missing_condition, _ = _sentinel_expressions(
1✔
1288
                    enum_type_info,
1289
                    var_ref="val",
1290
                    len_ref="val",
1291
                )
1292
                enum_functions.append(
1✔
1293
                    {
1294
                        "name": name,
1295
                        "func_name": f"{name}_in_enum",
1296
                        "arg_type_spec": _enum_arg_type_spec(type_info),
1297
                        "enum_values_name": enum_const_name,
1298
                        "use_trim": enum_category == "string",
1299
                        "missing_condition": missing_condition,
1300
                    }
1301
                )
1302
                if type_info.category == "array":
1✔
1303
                    enum_checks.append(
1✔
1304
                        {
1305
                            "name": name,
1306
                            "display_name": display_name,
1307
                            "func_name": f"{name}_in_enum",
1308
                            "is_array": True,
1309
                            "runtime_array": dynamic_array,
1310
                            "array_ref": f"this%{name}",
1311
                            "allocation_ref": f"this%{name}",
1312
                        }
1313
                    )
1314
                else:
1315
                    enum_checks.append(
1✔
1316
                        {
1317
                            "name": name,
1318
                            "display_name": display_name,
1319
                            "func_name": f"{name}_in_enum",
1320
                            "is_array": False,
1321
                            "element_ref": f"this%{name}",
1322
                        }
1323
                    )
1324

1325
            bounds_spec = _bounds_spec(prop, type_info)
1✔
1326
            if bounds_spec is not None:
1✔
1327
                bounds_category = bounds_spec["category"]
1✔
1328
                bounds_type_info = (
1✔
1329
                    _element_type_info(type_info) if type_info.category == "array" else type_info
1330
                )
1331
                min_value = bounds_spec["min_value"]
1✔
1332
                max_value = bounds_spec["max_value"]
1✔
1333
                min_exclusive = bounds_spec["min_exclusive"]
1✔
1334
                max_exclusive = bounds_spec["max_exclusive"]
1✔
1335
                min_name = None
1✔
1336
                max_name = None
1✔
1337
                if min_value is not None:
1✔
1338
                    min_name = f"{name}_min_excl" if min_exclusive else f"{name}_min"
1✔
1339
                    min_literal = _format_scalar_default(
1✔
1340
                        min_value, bounds_type_info.kind, bounds_category
1341
                    )
1342
                    bounds_parameters.append(
1✔
1343
                        f"{bounds_type_info.type_spec}, parameter, public :: "
1344
                        f"{min_name} = {min_literal}"
1345
                    )
1346
                if max_value is not None:
1✔
1347
                    max_name = f"{name}_max_excl" if max_exclusive else f"{name}_max"
1✔
1348
                    max_literal = _format_scalar_default(
1✔
1349
                        max_value, bounds_type_info.kind, bounds_category
1350
                    )
1351
                    bounds_parameters.append(
1✔
1352
                        f"{bounds_type_info.type_spec}, parameter, public :: "
1353
                        f"{max_name} = {max_literal}"
1354
                    )
1355
                _, missing_condition, uses_ieee = _sentinel_expressions(
1✔
1356
                    bounds_type_info,
1357
                    var_ref="val",
1358
                    len_ref="val",
1359
                )
1360
                if uses_ieee:
1✔
1361
                    requires_ieee = True
1✔
1362
                bounds_functions.append(
1✔
1363
                    {
1364
                        "name": name,
1365
                        "func_name": f"{name}_in_bounds",
1366
                        "arg_type_spec": bounds_type_info.arg_type_spec,
1367
                        "has_min": min_value is not None,
1368
                        "has_max": max_value is not None,
1369
                        "min_name": min_name,
1370
                        "max_name": max_name,
1371
                        "min_exclusive": min_exclusive,
1372
                        "max_exclusive": max_exclusive,
1373
                        "missing_condition": missing_condition,
1374
                    }
1375
                )
1376
                if type_info.category == "array":
1✔
1377
                    bounds_checks.append(
1✔
1378
                        {
1379
                            "name": name,
1380
                            "display_name": display_name,
1381
                            "func_name": f"{name}_in_bounds",
1382
                            "is_array": True,
1383
                            "runtime_array": dynamic_array,
1384
                            "array_ref": f"this%{name}",
1385
                            "allocation_ref": f"this%{name}",
1386
                        }
1387
                    )
1388
                else:
1389
                    bounds_checks.append(
1✔
1390
                        {
1391
                            "name": name,
1392
                            "display_name": display_name,
1393
                            "func_name": f"{name}_in_bounds",
1394
                            "is_array": False,
1395
                            "element_ref": f"this%{name}",
1396
                        }
1397
                    )
1398

1399
            is_array = type_info.category == "array"
1✔
1400
            if is_required:
1✔
1401
                if is_array:
1✔
1402
                    # Match namelist-buffer semantics: set assigns the provided
1403
                    # leading subsection and leaves completeness checks to is_valid.
1404
                    set_required_assignments.append(
1✔
1405
                        _render_partial_set_block(
1406
                            name,
1407
                            len(type_info.dimensions),
1408
                            partial_bounds or [],
1409
                        )
1410
                    )
1411
                else:
1412
                    set_required_assignments.append(f"this%{name} = {name}")
1✔
1413

1414
            if not is_required:
1✔
1415
                if is_array:
1✔
1416
                    # Match namelist-buffer semantics: set assigns the provided
1417
                    # leading subsection and leaves completeness checks to is_valid.
1418
                    block = _render_partial_set_block(
1✔
1419
                        name,
1420
                        len(type_info.dimensions),
1421
                        partial_bounds or [],
1422
                    )
1423
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
1✔
1424
                    set_present_assignment = (
1✔
1425
                        f"if (present({name})) then\n{indented_block}\nend if"
1426
                    )
1427
                else:
1428
                    set_present_assignment = f"if (present({name})) this%{name} = {name}"
1✔
1429
            else:
1430
                set_present_assignment = None
1✔
1431

1432
            array_rank = len(type_info.dimensions) if is_array else 0
1✔
1433
            element_condition: str | None = None
1✔
1434

1435
            if is_array and not has_default:
1✔
1436
                element_type = _element_type_info(type_info)
1✔
1437
                index_args = ", ".join(f"idx({idx})" for idx in range(1, array_rank + 1))
1✔
1438
                element_ref = f"this%{name}({index_args})"
1✔
1439
                _, element_condition, element_uses_ieee = _sentinel_expressions(
1✔
1440
                    element_type,
1441
                    var_ref=element_ref,
1442
                    len_ref=f"this%{name}",
1443
                )
1444
                if element_uses_ieee:
1✔
1445
                    requires_ieee = True
1✔
1446

1447
            if has_default or type_info.category == "boolean":
1✔
1448
                presence_cases.append(
1✔
1449
                    {
1450
                        "name": name,
1451
                        "display_name": display_name,
1452
                        "always_true": True,
1453
                        "sentinel_condition": None,
1454
                        "is_array": is_array,
1455
                        "runtime_array": dynamic_array,
1456
                        "rank": array_rank,
1457
                        "element_condition": element_condition,
1458
                    }
1459
                )
1460
            else:
1461
                if set_sentinel_condition is None:
1✔
1462
                    raise ValueError(f"missing sentinel condition for '{display_name}'")
×
1463
                presence_cases.append(
1✔
1464
                    {
1465
                        "name": name,
1466
                        "display_name": display_name,
1467
                        "always_true": False,
1468
                        "sentinel_condition": set_sentinel_condition,
1469
                        "is_array": is_array,
1470
                        "runtime_array": dynamic_array,
1471
                        "rank": array_rank,
1472
                        "element_condition": element_condition,
1473
                    }
1474
                )
1475

1476
            fields.append(
1✔
1477
                FieldSpec(
1478
                    order=index,
1479
                    name=name,
1480
                    title=title,
1481
                    description=description,
1482
                    declaration=declaration_with_doc,
1483
                    local_declaration=local_decl,
1484
                    required=is_required,
1485
                    sentinel_assignment=sentinel_assignment,
1486
                    sentinel_check=(
1487
                        f"if ({sentinel_condition}) error stop "
1488
                        f"\"{module_name}%from_file: '{name}' is required\""
1489
                        if sentinel_condition and is_required
1490
                        else None
1491
                    ),
1492
                    default_assignment=default_assignment,
1493
                    set_default_assignment=set_default_assignment,
1494
                    set_present_assignment=set_present_assignment,
1495
                    argument_declaration=argument_decl,
1496
                    type_category=type_info.category,
1497
                    runtime_sized_array=dynamic_array,
1498
                    rank=len(type_info.dimensions),
1499
                )
1500
            )
1501

1502
    except ValueError as exc:
1✔
1503
        if current_property is None:
1✔
1504
            raise
×
1505
        msg = str(exc)
1✔
1506
        if f"property '{current_property}'" in msg:
1✔
1507
            raise
×
1508
        raise ValueError(f"property '{current_property}': {msg}") from exc
1✔
1509
    if uses_partly_set and "NML_ERR_PARTLY_SET" not in helper_imports:
1✔
1510
        helper_imports.append("NML_ERR_PARTLY_SET")
1✔
1511
    required_flex_names = {entry["name"] for entry in flex_arrays if entry["required"]}
1✔
1512
    required_scalar_validations: list[str] = []
1✔
1513
    for name in required_fields:
1✔
1514
        if name in required_scalar_names:
1✔
1515
            required_scalar_validations.append(property_name_map[name])
1✔
1516
        elif name not in required_array_by_name and name not in required_flex_names:
1✔
1517
            required_scalar_validations.append(property_name_map[name])
1✔
1518
    required_array_validations = [
1✔
1519
        required_array_by_name[name]
1520
        for name in required_fields
1521
        if name in required_array_by_name
1522
    ]
1523
    namelist_vars = [field.name for field in fields]
1✔
1524
    required_fields_specs = [field for field in fields if field.required]
1✔
1525
    optional_fields_specs = [field for field in fields if not field.required]
1✔
1526

1527
    resolved_kind_module = kind_module or "iso_fortran_env"
1✔
1528
    if not isinstance(resolved_kind_module, str) or not resolved_kind_module:
1✔
1529
        raise ValueError("kind module must be a non-empty string")
×
1530

1531
    set_dims_arguments: list[dict[str, Any]] = []
1✔
1532
    candidate_names_in_use: set[str] = set()
1✔
1533
    for entry in runtime_dimensions:
1✔
1534
        candidate_base = f"candidate_{entry['name']}"
1✔
1535
        candidate_name = _unique_generated_name(candidate_base, candidate_names_in_use)
1✔
1536
        candidate_names_in_use.add(candidate_name.lower())
1✔
1537
        set_dims_arguments.append(
1✔
1538
            {
1539
                "name": entry["name"],
1540
                "default_name": entry["default_name"],
1541
                "local_name": entry["local_name"],
1542
                "arg_name": entry["name"],
1543
                "candidate_name": candidate_name,
1544
                "min_required": 1,
1545
            }
1546
        )
1547

1548
    candidate_name_map: dict[str, str] = {
1✔
1549
        str(entry["name"]): str(entry["candidate_name"]) for entry in set_dims_arguments
1550
    }
1551
    set_dims_extent_checks: list[dict[str, str]] = []
1✔
1552
    seen_extent_checks: set[tuple[str, str]] = set()
1✔
1553
    for requirement in runtime_default_extent_requirements:
1✔
1554
        factors: list[str] = []
1✔
1555
        requirement_dimensions = cast("list[str]", requirement["dimensions"])
1✔
1556
        for extent_dim in requirement_dimensions:
1✔
1557
            if extent_dim == ":":
1✔
1558
                continue
×
1559
            if _is_int_literal(extent_dim):
1✔
1560
                factors.append(extent_dim)
1✔
1561
            else:
1562
                factors.append(candidate_name_map.get(extent_dim.lower(), extent_dim))
1✔
1563
        if not factors:
1✔
1564
            continue
×
1565
        product_expr = " * ".join(factors)
1✔
1566
        if len(factors) > 1:
1✔
1567
            product_expr = f"({product_expr})"
1✔
1568
        required_elements = cast("int", requirement["required_elements"])
1✔
1569
        condition = f"{product_expr} < {required_elements}"
1✔
1570
        field_name = cast("str", requirement["field"])
1✔
1571
        extent_message = (
1✔
1572
            f"shape constants for '{field_name}' must allow at least "
1573
            f"{required_elements} default values"
1574
        )
1575
        extent_key = (condition, extent_message)
1✔
1576
        if extent_key in seen_extent_checks:
1✔
1577
            continue
×
1578
        seen_extent_checks.add(extent_key)
1✔
1579
        set_dims_extent_checks.append({"condition": condition, "message": extent_message})
1✔
1580

1581
    context = {
1✔
1582
        "module_name": module_name,
1583
        "type_name": type_name,
1584
        "type_prefix": module_name,
1585
        "doc_class": doc_class,
1586
        "brief_text": brief_text,
1587
        "details_text": details_text,
1588
        "module_doc": module_doc,
1589
        "namelist_name": namelist_name,
1590
        "fields": fields,
1591
        "runtime_dimensions": runtime_dimensions,
1592
        "runtime_allocations": runtime_allocations,
1593
        "runtime_deallocations": runtime_deallocations,
1594
        "runtime_local_allocations": runtime_local_allocations,
1595
        "namelist_vars": namelist_vars,
1596
        "sentinel_assignments": sentinel_assignments,
1597
        "default_assignments": default_assignments,
1598
        "default_parameters": default_parameters,
1599
        "enum_parameters": enum_parameters,
1600
        "bounds_parameters": bounds_parameters,
1601
        "local_init_assignments": local_init_assignments,
1602
        "required_scalar_validations": required_scalar_validations,
1603
        "required_array_validations": required_array_validations,
1604
        "flex_arrays": flex_arrays,
1605
        "assignments": [f"this%{field.name} = {field.name}" for field in fields],
1606
        "argument_list": [field.name for field in required_fields_specs + optional_fields_specs],
1607
        "required_argument_declarations": [
1608
            field.argument_declaration for field in required_fields_specs
1609
        ],
1610
        "optional_argument_declarations": [
1611
            field.argument_declaration for field in optional_fields_specs
1612
        ],
1613
        "set_dims_arguments": set_dims_arguments,
1614
        "set_dims_extent_checks": set_dims_extent_checks,
1615
        "set_required_assignments": set_required_assignments,
1616
        "set_optional_defaults": set_optional_defaults,
1617
        "set_optional_present": [
1618
            field.set_present_assignment
1619
            for field in optional_fields_specs
1620
            if field.set_present_assignment
1621
        ],
1622
        "enum_functions": enum_functions,
1623
        "enum_checks": enum_checks,
1624
        "bounds_functions": bounds_functions,
1625
        "bounds_checks": bounds_checks,
1626
        "derived_type_imports": derived_type_imports,
1627
        "derived_init_type_fields": derived_init_type_fields,
1628
        "derived_presence_blocks": derived_presence_blocks,
1629
        "derived_post_assignment_checks": derived_post_assignment_checks,
1630
        "kind_module": resolved_kind_module,
1631
        "kind_imports": _resolve_kind_imports(
1632
            kind_ids,
1633
            kind_map=kind_map,
1634
            kind_allowlist=kind_allowlist,
1635
        ),
1636
        "use_ieee": requires_ieee,
1637
        "helper_module": helper_module,
1638
        "helper_imports": helper_imports,
1639
        "presence_cases": presence_cases,
1640
        "flex_bound_vars": _sort_bound_vars(flex_bound_vars),
1641
        "f2py_handle_helpers": f2py_handle_helpers,
1642
    }
1643

1644
    return context
1✔
1645

1646

1647
def _derived_presence_cases(
1✔
1648
    *,
1649
    name: str,
1650
    display_name: str,
1651
    leaves: list[dict[str, Any]],
1652
    is_array: bool,
1653
    runtime_array: bool,
1654
    rank: int,
1655
) -> list[str]:
1656
    blocks: list[str] = []
1✔
1657
    idx_args = ", ".join(f"idx({index})" for index in range(1, rank + 1))
1✔
1658

1659
    def append_condition(
1✔
1660
        lines: list[str],
1661
        *,
1662
        prefix: str,
1663
        conditions: list[str],
1664
        operator: str,
1665
        suffix: str,
1666
    ) -> None:
1667
        if len(conditions) == 1:
1✔
1668
            lines.append(f"{prefix}{conditions[0]}{suffix}")
1✔
1669
            return
1✔
1670
        continuation = " " * (len(prefix) + 2)
1✔
1671
        for index, condition in enumerate(conditions):
1✔
1672
            if index == 0:
1✔
1673
                lines.append(f"{prefix}{condition} {operator} &")
1✔
1674
            elif index == len(conditions) - 1:
1✔
1675
                lines.append(f"{continuation}{condition}{suffix}")
1✔
1676
            else:
NEW
1677
                lines.append(f"{continuation}{condition} {operator} &")
×
1678

1679
    def array_prefix(lines: list[str]) -> None:
1✔
1680
        if runtime_array:
1✔
1681
            lines.extend(
1✔
1682
                [
1683
                    f"  if (.not. allocated(this%{name})) then",
1684
                    "    status = NML_ERR_NOT_SET",
1685
                    "    return",
1686
                    "  end if",
1687
                ]
1688
            )
1689

1690
    for leaf in leaves:
1✔
1691
        condition = leaf["missing_condition"]
1✔
1692
        lines = [f'case ("{name}%{leaf["name"]}")']
1✔
1693
        if is_array:
1✔
1694
            array_prefix(lines)
1✔
1695
            lines.extend(
1✔
1696
                [
1697
                    "  if (present(idx)) then",
1698
                    f"    status = idx_check(idx, lbound(this%{name}), ubound(this%{name}), &",
1699
                    f'      "{display_name}", errmsg)',
1700
                    "    if (status /= NML_OK) return",
1701
                ]
1702
            )
1703
            if condition is not None:
1✔
1704
                indexed = str(condition).replace(
1✔
1705
                    f"this%{name}%{leaf['name']}",
1706
                    f"this%{name}({idx_args})%{leaf['name']}",
1707
                )
1708
                lines.append(f"    if ({indexed}) status = NML_ERR_NOT_SET")
1✔
1709
            lines.append("  else")
1✔
1710
            if condition is not None:
1✔
1711
                lines.append(f"    if (all({condition})) status = NML_ERR_NOT_SET")
1✔
1712
            lines.append("  end if")
1✔
1713
        else:
1714
            lines.extend(
1✔
1715
                [
1716
                    "  if (present(idx)) then",
1717
                    "    status = NML_ERR_INVALID_INDEX",
1718
                    "    if (present(errmsg)) "
1719
                    f'errmsg = "index not supported for \'{display_name}\'"',
1720
                    "    return",
1721
                    "  end if",
1722
                ]
1723
            )
1724
            if condition is not None:
1✔
1725
                lines.append(f"  if ({condition}) status = NML_ERR_NOT_SET")
1✔
1726
        blocks.append("\n".join(lines))
1✔
1727

1728
    required_conditions = [
1✔
1729
        str(leaf["missing_condition"])
1730
        for leaf in leaves
1731
        if leaf["required"] and leaf["missing_condition"] is not None
1732
    ]
1733
    aggregate_conditions = required_conditions or [
1✔
1734
        str(leaf["missing_condition"])
1735
        for leaf in leaves
1736
        if leaf["missing_condition"] is not None
1737
    ]
1738
    lines = [f'case ("{name}")']
1✔
1739
    if is_array:
1✔
1740
        array_prefix(lines)
1✔
1741
        lines.extend(
1✔
1742
            [
1743
                "  if (present(idx)) then",
1744
                f"    status = idx_check(idx, lbound(this%{name}), ubound(this%{name}), &",
1745
                f'      "{display_name}", errmsg)',
1746
                "    if (status /= NML_OK) return",
1747
            ]
1748
        )
1749
        indexed_conditions = [
1✔
1750
            condition.replace(f"this%{name}%", f"this%{name}({idx_args})%")
1751
            for condition in aggregate_conditions
1752
        ]
1753
        if indexed_conditions:
1✔
1754
            append_condition(
1✔
1755
                lines,
1756
                prefix="    if (",
1757
                conditions=indexed_conditions,
1758
                operator=".and.",
1759
                suffix=") then",
1760
            )
1761
            lines.append("      status = NML_ERR_NOT_SET")
1✔
1762
            if required_conditions and len(indexed_conditions) > 1:
1✔
1763
                append_condition(
1✔
1764
                    lines,
1765
                    prefix="    else if (",
1766
                    conditions=indexed_conditions,
1767
                    operator=".or.",
1768
                    suffix=") then",
1769
                )
1770
                lines.append("      status = NML_ERR_PARTLY_SET")
1✔
1771
            lines.append("    end if")
1✔
1772
        lines.append("  else")
1✔
1773
        if aggregate_conditions:
1✔
1774
            append_condition(
1✔
1775
                lines,
1776
                prefix="    if (all(",
1777
                conditions=aggregate_conditions,
1778
                operator=".and.",
1779
                suffix=")) then",
1780
            )
1781
            lines.append("      status = NML_ERR_NOT_SET")
1✔
1782
            if required_conditions and (len(aggregate_conditions) > 1 or is_array):
1✔
1783
                append_condition(
1✔
1784
                    lines,
1785
                    prefix="    else if (any(",
1786
                    conditions=aggregate_conditions,
1787
                    operator=".or.",
1788
                    suffix=")) then",
1789
                )
1790
                lines.append("      status = NML_ERR_PARTLY_SET")
1✔
1791
            lines.append("    end if")
1✔
1792
        lines.append("  end if")
1✔
1793
    else:
1794
        lines.extend(
1✔
1795
            [
1796
                "  if (present(idx)) then",
1797
                "    status = NML_ERR_INVALID_INDEX",
1798
                f'    if (present(errmsg)) errmsg = "index not supported for \'{display_name}\'"',
1799
                "    return",
1800
                "  end if",
1801
            ]
1802
        )
1803
        if aggregate_conditions:
1✔
1804
            append_condition(
1✔
1805
                lines,
1806
                prefix="  if (",
1807
                conditions=aggregate_conditions,
1808
                operator=".and.",
1809
                suffix=") then",
1810
            )
1811
            lines.append("    status = NML_ERR_NOT_SET")
1✔
1812
            if len(aggregate_conditions) > 1 and required_conditions:
1✔
1813
                append_condition(
1✔
1814
                    lines,
1815
                    prefix="  else if (",
1816
                    conditions=aggregate_conditions,
1817
                    operator=".or.",
1818
                    suffix=") then",
1819
                )
1820
                lines.append("    status = NML_ERR_PARTLY_SET")
1✔
1821
            lines.append("  end if")
1✔
1822
    blocks.append("\n".join(lines))
1✔
1823
    return blocks
1✔
1824

1825

1826
def _ordered_unique(values: Iterable[Any]) -> list[Any]:
1✔
1827
    seen: set[Any] = set()
1✔
1828
    ordered: list[Any] = []
1✔
1829
    for value in values:
1✔
1830
        if value not in seen:
1✔
1831
            seen.add(value)
1✔
1832
            ordered.append(value)
1✔
1833
    return ordered
1✔
1834

1835

1836
def _reject_runtime_dimension_lengths(
1✔
1837
    prop: dict[str, Any],
1838
    dimensions: dict[str, int],
1839
) -> None:
1840
    if not dimensions:
1✔
1841
        return
1✔
1842
    if prop.get("type") == "string":
1✔
1843
        length = prop.get("x-fortran-len")
1✔
1844
        if isinstance(length, str) and length.strip().lower() in dimensions:
1✔
1845
            raise ValueError(f"dimension '{length.strip()}' cannot be used as x-fortran-len")
1✔
1846
    if prop.get("type") == "array":
1✔
1847
        items = prop.get("items")
1✔
1848
        if isinstance(items, dict):
1✔
1849
            _reject_runtime_dimension_lengths(items, dimensions)
1✔
1850

1851

1852
def _resolve_kind_imports(
1✔
1853
    kind_ids: list[str],
1854
    *,
1855
    kind_map: dict[str, str] | None,
1856
    kind_allowlist: Iterable[str] | None,
1857
) -> list[str]:
1858
    allowlist = set(kind_allowlist) if kind_allowlist is not None else None
1✔
1859
    imports: list[str] = []
1✔
1860
    target_aliases: dict[str, str] = {}
1✔
1861
    for kind_id in _ordered_unique(kind_ids):
1✔
1862
        target = kind_id
1✔
1863
        if kind_map is not None and kind_id in kind_map:
1✔
1864
            mapped = kind_map[kind_id]
1✔
1865
            if not isinstance(mapped, str):
1✔
1866
                raise ValueError(f"kind map target for '{kind_id}' must be a string")
×
1867
            target = mapped
1✔
1868
        elif allowlist is not None and kind_id not in allowlist:
1✔
1869
            raise ValueError(f"kind '{kind_id}' not present in kind map or kind module list")
×
1870

1871
        if allowlist is not None and target not in allowlist:
1✔
1872
            raise ValueError(
×
1873
                f"kind map target '{target}' for '{kind_id}' not present in kind module list"
1874
            )
1875

1876
        if target != kind_id:
1✔
1877
            existing = target_aliases.get(target)
1✔
1878
            if existing is not None and existing != kind_id:
1✔
1879
                raise ValueError(
×
1880
                    f"kind map target '{target}' is shared by '{existing}' and '{kind_id}'"
1881
                )
1882
            target_aliases[target] = kind_id
1✔
1883
            imports.append(f"{kind_id}=>{target}")
1✔
1884
        else:
1885
            imports.append(kind_id)
1✔
1886
    return imports
1✔
1887

1888

1889
def _field_type_info(
1✔
1890
    prop: dict[str, Any],
1891
    constants: dict[str, int] | None,
1892
) -> FieldTypeInfo:
1893
    prop_type = prop.get("type")
1✔
1894
    if prop_type == "array":
1✔
1895
        dimensions: list[str] = []
1✔
1896
        current = prop
1✔
1897
        while current.get("type") == "array":
1✔
1898
            dimensions.extend(_extract_dimensions(current))
1✔
1899
            items = current.get("items")
1✔
1900
            if not isinstance(items, dict):
1✔
1901
                raise ValueError("array property must define 'items'")
×
1902
            if items.get("type") == "array":
1✔
1903
                raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
1904
            current = items
1✔
1905
        if current.get("type") == "object":
1✔
1906
            type_name = _derived_type_name(current)
1✔
1907
            return FieldTypeInfo(
1✔
1908
                type_spec=f"type({type_name})",
1909
                arg_type_spec=f"type({type_name})",
1910
                dimensions=dimensions,
1911
                kind=None,
1912
                category="array",
1913
                element_category="derived",
1914
            )
1915
        scalar = _scalar_type_info(current, constants)
1✔
1916
        return FieldTypeInfo(
1✔
1917
            type_spec=scalar.type_spec,
1918
            arg_type_spec=scalar.arg_type_spec,
1919
            dimensions=dimensions,
1920
            kind=scalar.kind,
1921
            category="array",
1922
            length_expr=scalar.length_expr,
1923
            element_category=scalar.category,
1924
        )
1925

1926
    if prop_type == "object":
1✔
1927
        type_name = _derived_type_name(prop)
1✔
1928
        return FieldTypeInfo(
1✔
1929
            type_spec=f"type({type_name})",
1930
            arg_type_spec=f"type({type_name})",
1931
            dimensions=[],
1932
            kind=None,
1933
            category="derived",
1934
        )
1935
    scalar = _scalar_type_info(prop, constants)
1✔
1936
    return FieldTypeInfo(
1✔
1937
        type_spec=scalar.type_spec,
1938
        arg_type_spec=scalar.arg_type_spec,
1939
        dimensions=[],
1940
        kind=scalar.kind,
1941
        category=scalar.category,
1942
        length_expr=scalar.length_expr,
1943
        element_category=None,
1944
    )
1945

1946

1947
def _derived_schema(prop: dict[str, Any]) -> dict[str, Any] | None:
1✔
1948
    if prop.get("type") == "object":
1✔
1949
        return prop
1✔
1950
    if prop.get("type") == "array":
1✔
1951
        items = prop.get("items")
1✔
1952
        if isinstance(items, dict) and items.get("type") == "object":
1✔
1953
            return items
1✔
1954
    return None
1✔
1955

1956

1957
def _derived_type_name(schema: dict[str, Any]) -> str:
1✔
1958
    type_name = schema.get("x-fortran-type")
1✔
1959
    if not isinstance(type_name, str) or not type_name.strip():
1✔
NEW
1960
        raise ValueError("derived object must define non-empty 'x-fortran-type'")
×
1961
    return type_name.strip()
1✔
1962

1963

1964
def _derived_origin(schema: dict[str, Any]) -> dict[str, Any]:
1✔
1965
    origin = schema.get(DERIVED_REF_ORIGIN_KEY)
1✔
1966
    if not isinstance(origin, dict):
1✔
NEW
1967
        raise ValueError("derived object must originate from a normalized definition")
×
1968
    raw_identity = origin.get("identity")
1✔
1969
    definition = origin.get("definition")
1✔
1970
    if (
1✔
1971
        not isinstance(raw_identity, list)
1972
        or len(raw_identity) != 2
1973
        or not all(isinstance(value, str) for value in raw_identity)
1974
        or not isinstance(definition, dict)
1975
    ):
NEW
1976
        raise ValueError("derived object has invalid reference origin metadata")
×
1977
    return {"identity": (raw_identity[0], raw_identity[1]), "definition": definition}
1✔
1978

1979

1980
def _element_type_info(type_info: FieldTypeInfo) -> FieldTypeInfo:
1✔
1981
    if type_info.category != "array":
1✔
1982
        raise ValueError("element type info requires an array field")
×
1983
    element_category = type_info.element_category
1✔
1984
    if element_category is None:
1✔
1985
        raise ValueError("array field missing element category")
×
1986
    return FieldTypeInfo(
1✔
1987
        type_spec=type_info.type_spec,
1988
        arg_type_spec=type_info.type_spec,
1989
        dimensions=[],
1990
        kind=type_info.kind,
1991
        category=element_category,
1992
        length_expr=type_info.length_expr,
1993
        element_category=None,
1994
    )
1995

1996

1997
def _enum_category(type_info: FieldTypeInfo) -> str:
1✔
1998
    if type_info.category == "array":
1✔
1999
        if type_info.element_category is None:
1✔
2000
            raise ValueError("array field missing element category")
×
2001
        return type_info.element_category
1✔
2002
    return type_info.category
1✔
2003

2004

2005
def _enum_arg_type_spec(type_info: FieldTypeInfo) -> str:
1✔
2006
    category = _enum_category(type_info)
1✔
2007
    if category == "string":
1✔
2008
        return "character(len=*)"
1✔
2009
    return type_info.type_spec
1✔
2010

2011

2012
def _scalar_type_info(
1✔
2013
    prop: dict[str, Any],
2014
    constants: dict[str, int] | None,
2015
) -> ScalarTypeInfo:
2016
    prop_type = prop.get("type")
1✔
2017
    if prop_type == "string":
1✔
2018
        length = prop.get("x-fortran-len")
1✔
2019
        if isinstance(length, bool):
1✔
2020
            raise ValueError("string property must define integer 'x-fortran-len'")
×
2021
        if isinstance(length, int):
1✔
2022
            if length <= 0:
1✔
2023
                raise ValueError("string length must be positive")
×
2024
            length_expr = str(length)
1✔
2025
        elif isinstance(length, str):
1✔
2026
            length_expr = length.strip()
1✔
2027
            if not length_expr:
1✔
2028
                raise ValueError("string length must be a non-empty value")
×
2029
            _validate_length_token(length_expr)
1✔
2030
            if _is_int_literal(length_expr):
1✔
2031
                if int(length_expr) <= 0:
×
2032
                    raise ValueError("string length must be positive")
×
2033
            else:
2034
                length_key = length_expr.lower()
1✔
2035
                if constants is None or length_key not in constants:
1✔
2036
                    raise ValueError(
×
2037
                        f"string length constant '{length_expr}' is not defined in config"
2038
                    )
2039
                value = constants[length_key]
1✔
2040
                if isinstance(value, bool) or not isinstance(value, int):
1✔
2041
                    raise ValueError(f"string length constant '{length_expr}' must be an integer")
×
2042
                if value <= 0:
1✔
2043
                    raise ValueError(f"string length constant '{length_expr}' must be positive")
×
2044
        else:
2045
            raise ValueError("string property must define integer 'x-fortran-len'")
×
2046
        return ScalarTypeInfo(
1✔
2047
            type_spec=f"character(len={length_expr})",
2048
            arg_type_spec="character(len=*)",
2049
            kind=None,
2050
            category="string",
2051
            length_expr=length_expr,
2052
        )
2053
    if prop_type == "integer":
1✔
2054
        kind = prop.get("x-fortran-kind")
1✔
2055
        if kind is None:
1✔
2056
            return ScalarTypeInfo(
1✔
2057
                type_spec="integer",
2058
                arg_type_spec="integer",
2059
                kind=None,
2060
                category="integer",
2061
            )
2062
        if not isinstance(kind, str) or not kind.strip():
1✔
2063
            raise ValueError("integer property 'x-fortran-kind' must be a non-empty string")
×
2064
        return ScalarTypeInfo(
1✔
2065
            type_spec=f"integer({kind})",
2066
            arg_type_spec=f"integer({kind})",
2067
            kind=kind,
2068
            category="integer",
2069
        )
2070
    if prop_type == "number":
1✔
2071
        kind = prop.get("x-fortran-kind")
1✔
2072
        if kind is None:
1✔
2073
            return ScalarTypeInfo(
1✔
2074
                type_spec="real",
2075
                arg_type_spec="real",
2076
                kind=None,
2077
                category="real",
2078
            )
2079
        if not isinstance(kind, str) or not kind.strip():
1✔
2080
            raise ValueError("number property 'x-fortran-kind' must be a non-empty string")
×
2081
        return ScalarTypeInfo(
1✔
2082
            type_spec=f"real({kind})",
2083
            arg_type_spec=f"real({kind})",
2084
            kind=kind,
2085
            category="real",
2086
        )
2087
    if prop_type == "boolean":
1✔
2088
        return ScalarTypeInfo(
1✔
2089
            type_spec="logical",
2090
            arg_type_spec="logical",
2091
            kind=None,
2092
            category="boolean",
2093
        )
2094
    raise ValueError(f"unsupported property type '{prop_type}'")
×
2095

2096

2097
def _extract_dimensions(prop: dict[str, Any]) -> list[str]:
1✔
2098
    shape = prop.get("x-fortran-shape")
1✔
2099
    if isinstance(shape, bool):
1✔
2100
        raise ValueError("array property 'x-fortran-shape' must not be a boolean")
×
2101
    if isinstance(shape, int):
1✔
2102
        return [str(shape)]
1✔
2103
    if isinstance(shape, str):
1✔
2104
        dim = shape.strip()
1✔
2105
        if not dim:
1✔
2106
            raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
2107
        _validate_dimension_token(dim)
1✔
2108
        return [dim]
1✔
2109
    if isinstance(shape, list):
1✔
2110
        dimensions: list[str] = []
1✔
2111
        for dim in shape:
1✔
2112
            if isinstance(dim, bool):
1✔
2113
                raise ValueError("array property 'x-fortran-shape' must not include booleans")
×
2114
            if isinstance(dim, int):
1✔
2115
                dim_literal = str(dim)
1✔
2116
            elif isinstance(dim, str):
1✔
2117
                dim_literal = dim.strip()
1✔
2118
                if not dim_literal:
1✔
2119
                    raise ValueError("array property 'x-fortran-shape' entries must be non-empty")
×
2120
            else:
2121
                raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
2122
            _validate_dimension_token(dim_literal)
1✔
2123
            dimensions.append(dim_literal)
1✔
2124
        return dimensions
1✔
2125
    if shape is None:
1✔
2126
        raise ValueError("array property must define 'x-fortran-shape'")
1✔
2127
    raise ValueError("array property 'x-fortran-shape' must be an int, string, or list")
×
2128

2129

2130
def _is_int_literal(value: str) -> bool:
1✔
2131
    try:
1✔
2132
        int(value)
1✔
2133
    except ValueError:
1✔
2134
        return False
1✔
2135
    return True
1✔
2136

2137

2138
def _validate_dimension_token(dim: str) -> None:
1✔
2139
    if dim == ":":
1✔
2140
        return
×
2141
    if _is_int_literal(dim):
1✔
2142
        return
1✔
2143
    if FORTRAN_IDENTIFIER.match(dim):
1✔
2144
        return
1✔
2145
    raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
2146

2147

2148
def _validate_length_token(length_expr: str) -> None:
1✔
2149
    if _is_int_literal(length_expr):
1✔
2150
        return
×
2151
    if FORTRAN_IDENTIFIER.match(length_expr):
1✔
2152
        return
1✔
2153
    raise ValueError("string length must be an integer literal or identifier")
×
2154

2155

2156
def _parse_flex_dim(prop: dict[str, Any], type_info: FieldTypeInfo) -> int:
1✔
2157
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
2158
    if flex_raw is None:
1✔
2159
        flex_value = 0
1✔
2160
    else:
2161
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
1✔
2162
            raise ValueError("x-fortran-flex-tail-dims must be an integer")
×
2163
        flex_value = flex_raw
1✔
2164
    if flex_value < 0:
1✔
2165
        raise ValueError("x-fortran-flex-tail-dims must be >= 0")
×
2166
    if flex_value == 0:
1✔
2167
        return 0
1✔
2168
    if type_info.category != "array":
1✔
2169
        raise ValueError("x-fortran-flex-tail-dims is only supported for arrays")
1✔
2170
    if flex_value > len(type_info.dimensions):
1✔
2171
        raise ValueError("x-fortran-flex-tail-dims must not exceed array rank")
1✔
2172
    if any(dim == ":" for dim in type_info.dimensions):
1✔
2173
        raise ValueError(
×
2174
            "x-fortran-flex-tail-dims does not support deferred-size dimensions"
2175
        )
2176
    return flex_value
1✔
2177

2178

2179
def _collect_dimension_constants(
1✔
2180
    dimensions: list[str],
2181
    constants: dict[str, int] | None,
2182
) -> list[str]:
2183
    used: list[str] = []
1✔
2184
    for dim in dimensions:
1✔
2185
        if dim == ":" or _is_int_literal(dim):
1✔
2186
            continue
1✔
2187
        if not FORTRAN_IDENTIFIER.match(dim):
1✔
2188
            raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
2189
        dim_key = dim.lower()
1✔
2190
        if constants is None or dim_key not in constants:
1✔
2191
            raise ValueError(f"dimension constant '{dim}' is not defined in config")
1✔
2192
        value = constants[dim_key]
1✔
2193
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2194
            raise ValueError(f"dimension constant '{dim}' must be an integer")
×
2195
        if dim not in used:
1✔
2196
            used.append(dim)
1✔
2197
    return used
1✔
2198

2199

2200
def _render_declaration(type_spec: str, dimensions: list[str], name: str) -> str:
1✔
2201
    parts = [type_spec]
1✔
2202
    if dimensions:
1✔
2203
        dims = ", ".join(dimensions)
1✔
2204
        parts.append(f"dimension({dims})")
1✔
2205
    return f"{', '.join(parts)} :: {name}"
1✔
2206

2207

2208
def _render_runtime_declaration(
1✔
2209
    type_info: FieldTypeInfo,
2210
    name: str,
2211
) -> str:
2212
    if type_info.category == "string":
1✔
2213
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
2214
    if type_info.category != "array":
1✔
2215
        raise ValueError(
×
2216
            "runtime declaration is only supported for arrays, "
2217
            f"got category '{type_info.category}'"
2218
        )
2219

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

2223

2224
def _render_runtime_local_declaration(
1✔
2225
    type_info: FieldTypeInfo,
2226
    name: str,
2227
    *,
2228
    runtime_dimensions: list[str],
2229
) -> str:
2230
    if type_info.category == "string":
1✔
2231
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
2232
    if type_info.category != "array":
1✔
2233
        raise ValueError(
×
2234
            "runtime local declaration is only supported for arrays, "
2235
            f"got category '{type_info.category}'"
2236
        )
2237

2238
    type_spec = type_info.type_spec
1✔
2239
    dims = ", ".join(":" for _ in runtime_dimensions)
1✔
2240
    return f"{type_spec}, allocatable, dimension({dims}) :: {name}"
1✔
2241

2242

2243
def _render_runtime_allocations(
1✔
2244
    type_info: FieldTypeInfo,
2245
    name: str,
2246
    *,
2247
    runtime_dimensions: list[str],
2248
    runtime_length_expr: str | None,
2249
    target_prefix: str = "",
2250
) -> list[str]:
2251
    lines: list[str] = []
1✔
2252
    target_ref = f"{target_prefix}{name}"
1✔
2253
    if type_info.category != "array":
1✔
2254
        raise ValueError("runtime allocation is only supported for arrays")
×
2255
    if any(dim == ":" for dim in runtime_dimensions):
1✔
2256
        raise ValueError("runtime-sized arrays do not support deferred-size dimensions")
×
2257

2258
    lines.append(f"if (allocated({target_ref})) deallocate({target_ref})")
1✔
2259
    dims_expr = ", ".join(runtime_dimensions)
1✔
2260
    if type_info.element_category == "string" and runtime_length_expr is not None:
1✔
2261
        lines.append(f"allocate(character(len={runtime_length_expr}) :: {target_ref}({dims_expr}))")
1✔
2262
    else:
2263
        lines.append(f"allocate({target_ref}({dims_expr}))")
1✔
2264
    return lines
1✔
2265

2266

2267
def _render_runtime_deallocations(name: str, *, target_prefix: str = "") -> list[str]:
1✔
2268
    target_ref = f"{target_prefix}{name}"
1✔
2269
    return [f"if (allocated({target_ref})) deallocate({target_ref})"]
1✔
2270

2271

2272
def _render_runtime_local_allocations(
1✔
2273
    type_info: FieldTypeInfo,
2274
    name: str,
2275
    *,
2276
    runtime_dimensions: list[str],
2277
    runtime_length_expr: str | None,
2278
) -> list[str]:
2279
    return _render_runtime_allocations(
1✔
2280
        type_info,
2281
        name,
2282
        runtime_dimensions=runtime_dimensions,
2283
        runtime_length_expr=runtime_length_expr,
2284
    )
2285

2286

2287
def _array_section_ref(base_ref: str, rank: int) -> str:
1✔
2288
    dims = ", ".join(":" for _ in range(rank))
1✔
2289
    return f"{base_ref}({dims})"
1✔
2290

2291

2292
def _render_argument_declaration(
1✔
2293
    *,
2294
    name: str,
2295
    type_info: FieldTypeInfo,
2296
    is_required: bool,
2297
    dimensions: list[str] | None = None,
2298
    doc: str | None = None,
2299
) -> str:
2300
    intent = "intent(in)"
1✔
2301
    parts = [type_info.arg_type_spec]
1✔
2302
    arg_dimensions = dimensions if dimensions is not None else type_info.dimensions
1✔
2303
    if arg_dimensions:
1✔
2304
        dims = ", ".join(arg_dimensions)
1✔
2305
        parts.append(f"dimension({dims})")
1✔
2306
    if not is_required:
1✔
2307
        parts.append(intent)
1✔
2308
        parts.append("optional")
1✔
2309
        decl = f"{', '.join(parts[:-1])}, {parts[-1]} :: {name}"
1✔
2310
    else:
2311
        parts.append(intent)
1✔
2312
        decl = f"{', '.join(parts)} :: {name}"
1✔
2313
    if doc:
1✔
2314
        decl = f"{decl} !< {doc}"
1✔
2315
    return decl
1✔
2316

2317

2318
def _sentinel_comment(type_info: FieldTypeInfo, *, required: bool) -> str:
1✔
2319
    label = "required" if required else "optional"
1✔
2320
    category = type_info.category
1✔
2321
    if category == "array":
1✔
2322
        element = type_info.element_category or "array"
1✔
2323
        return f" ! sentinel for {label} {element} array"
1✔
2324
    if category == "string":
1✔
2325
        if required:
1✔
2326
            return " ! NULL string as sentinel for required string"
1✔
2327
        return " ! sentinel for optional string"
1✔
2328
    if category == "integer":
1✔
2329
        return f" ! sentinel for {label} integer"
1✔
2330
    if category == "real":
1✔
2331
        return f" ! sentinel for {label} real"
1✔
2332
    return ""
×
2333

2334

2335
def _render_sentinel_assignment(
1✔
2336
    type_info: FieldTypeInfo,
2337
    *,
2338
    target_ref: str,
2339
    value_expr: str,
2340
    comment: str,
2341
) -> str:
2342
    if type_info.category == "string":
1✔
2343
        return _render_string_sentinel_assignment(
1✔
2344
            target_ref=target_ref,
2345
            value_expr=value_expr,
2346
            comment=comment,
2347
        )
2348
    if type_info.category == "array" and type_info.element_category == "string":
1✔
2349
        return _render_string_array_sentinel_assignment(
1✔
2350
            target_ref=target_ref,
2351
            value_expr=value_expr,
2352
            comment=comment,
2353
        )
2354
    return f"{target_ref} = {value_expr}{comment}"
1✔
2355

2356

2357
def _render_string_sentinel_assignment(
1✔
2358
    *,
2359
    target_ref: str,
2360
    value_expr: str,
2361
    comment: str,
2362
) -> str:
2363
    return f"{target_ref} = {value_expr}{comment}"
1✔
2364

2365

2366
def _render_string_array_sentinel_assignment(
1✔
2367
    *,
2368
    target_ref: str,
2369
    value_expr: str,
2370
    comment: str,
2371
) -> str:
2372
    return f"{target_ref} = {value_expr}{comment}"
1✔
2373

2374

2375
def _sentinel_expressions(
1✔
2376
    type_info: FieldTypeInfo,
2377
    *,
2378
    var_ref: str,
2379
    len_ref: str | None = None,
2380
) -> tuple[str, str, bool]:
2381
    category = type_info.category
1✔
2382
    if category == "array":
1✔
2383
        element = type_info.element_category
1✔
2384
        if element == "string":
1✔
2385
            return "achar(0)", f"all({var_ref} == achar(0))", False
1✔
2386
        if element == "integer":
1✔
2387
            return f"-huge({var_ref})", f"all({var_ref} == -huge({var_ref}))", False
1✔
2388
        if element == "real":
1✔
2389
            return (
1✔
2390
                f"ieee_value({var_ref}, ieee_quiet_nan)",
2391
                f"all(ieee_is_nan({var_ref}))",
2392
                True,
2393
            )
2394
        if element == "boolean":
×
2395
            raise ValueError("boolean arrays cannot use sentinels")
×
2396
        raise ValueError(f"unsupported sentinel array element '{element}'")
×
2397
    if category == "string":
1✔
2398
        return "achar(0)", f"{var_ref} == achar(0)", False
1✔
2399
    if category == "integer":
1✔
2400
        return f"-huge({var_ref})", f"{var_ref} == -huge({var_ref})", False
1✔
2401
    if category == "real":
1✔
2402
        return (
1✔
2403
            f"ieee_value({var_ref}, ieee_quiet_nan)",
2404
            f"ieee_is_nan({var_ref})",
2405
            True,
2406
        )
2407
    if category == "boolean":
×
2408
        raise ValueError("boolean values cannot use sentinels")
×
2409
    raise ValueError(f"unsupported sentinel category '{category}'")
×
2410

2411

2412
def _element_missing_expression(
1✔
2413
    category: str,
2414
    *,
2415
    var_ref: str,
2416
    len_ref: str | None = None,
2417
) -> tuple[str, bool]:
2418
    if category == "string":
1✔
2419
        return f"{var_ref} == achar(0)", False
×
2420
    if category == "integer":
1✔
2421
        return f"{var_ref} == -huge({var_ref})", False
1✔
2422
    if category == "real":
1✔
2423
        return f"ieee_is_nan({var_ref})", True
1✔
2424
    raise ValueError(f"unsupported missing category '{category}'")
×
2425

2426

2427
def _array_missing_conditions(
1✔
2428
    element_category: str,
2429
    *,
2430
    var_ref: str,
2431
    len_ref: str | None = None,
2432
) -> tuple[str, str, bool]:
2433
    missing_expr, uses_ieee = _element_missing_expression(
1✔
2434
        element_category, var_ref=var_ref, len_ref=len_ref
2435
    )
2436
    return f"all({missing_expr})", f"any({missing_expr})", uses_ieee
1✔
2437

2438

2439
def _slice_ref(name: str, rank: int, dim: int, index_var: str) -> str:
1✔
2440
    dims = [":" for _ in range(rank)]
1✔
2441
    dims[dim - 1] = index_var
1✔
2442
    return f"this%{name}({', '.join(dims)})"
1✔
2443

2444

2445
def _flex_bound_vars(dim: int) -> tuple[str, str]:
1✔
2446
    return f"lb_{dim}", f"ub_{dim}"
1✔
2447

2448

2449
def _slice_ref_bounds(
1✔
2450
    name: str,
2451
    rank: int,
2452
    flex_dims: list[int],
2453
    lb_vars: dict[int, str],
2454
    ub_vars: dict[int, str],
2455
) -> str:
2456
    dims: list[str] = []
1✔
2457
    for dim in range(1, rank + 1):
1✔
2458
        if dim in flex_dims:
1✔
2459
            dims.append(f"{lb_vars[dim]}:{ub_vars[dim]}")
1✔
2460
        else:
2461
            dims.append(":")
1✔
2462
    return f"this%{name}({', '.join(dims)})"
1✔
2463

2464

2465
def _render_partial_set_block(
1✔
2466
    name: str,
2467
    rank: int,
2468
    bounds: list[dict[str, Any]],
2469
) -> str:
2470
    lb_vars = {entry["dim"]: entry["lb_var"] for entry in bounds}
1✔
2471
    ub_vars = {entry["dim"]: entry["ub_var"] for entry in bounds}
1✔
2472
    dims_all = [entry["dim"] for entry in bounds]
1✔
2473
    lines: list[str] = []
1✔
2474
    for entry in bounds:
1✔
2475
        dim = entry["dim"]
1✔
2476
        lb_var = entry["lb_var"]
1✔
2477
        ub_var = entry["ub_var"]
1✔
2478
        lines.append(f"if (size({name}, {dim}) > size(this%{name}, {dim})) then")
1✔
2479
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
2480
        lines.append(
1✔
2481
            f"  if (present(errmsg)) errmsg = \"dimension {dim} exceeds bounds for '{name}'\""
2482
        )
2483
        lines.append("  return")
1✔
2484
        lines.append("end if")
1✔
2485
        lines.append(f"{lb_var} = lbound(this%{name}, {dim})")
1✔
2486
        lines.append(f"{ub_var} = {lb_var} + size({name}, {dim}) - 1")
1✔
2487
    target_ref = _slice_ref_bounds(name, rank, dims_all, lb_vars, ub_vars)
1✔
2488
    lines.append(f"{target_ref} = {name}")
1✔
2489
    return "\n".join(lines)
1✔
2490

2491

2492
def _sort_bound_vars(values: set[str]) -> list[str]:
1✔
2493
    def sort_key(name: str) -> tuple[str, int]:
1✔
2494
        prefix, _, suffix = name.partition("_")
1✔
2495
        try:
1✔
2496
            return prefix, int(suffix)
1✔
2497
        except ValueError:
×
2498
            return prefix, 0
×
2499

2500
    return sorted(values, key=sort_key)
1✔
2501

2502

2503
def _format_reshape_assignment(name: str, arguments: list[str]) -> str:
1✔
2504
    lines = [f"this%{name} = reshape( &"]
1✔
2505
    for index, arg in enumerate(arguments):
1✔
2506
        suffix = ", &" if index < len(arguments) - 1 else ")"
1✔
2507
        lines.append(f"  {arg}{suffix}")
1✔
2508
    return "\n".join(lines)
1✔
2509

2510

2511
def _format_default(
1✔
2512
    value: Any,
2513
    type_info: FieldTypeInfo,
2514
    prop: dict[str, Any],
2515
    constants: dict[str, int] | None = None,
2516
) -> str:
2517
    if type_info.category == "array":
1✔
2518
        if not isinstance(value, list):
×
2519
            raise ValueError("array default must be a list")
×
2520
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
2521
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
2522
        elements = [
×
2523
            _format_scalar_default(element, type_info.kind, type_info.element_category)
2524
            for element in array_default.source_values
2525
        ]
2526
        if (
×
2527
            len(type_info.dimensions) == 1
2528
            and array_default.order_values is None
2529
            and array_default.pad_values is None
2530
        ):
2531
            return f"[{', '.join(elements)}]"
×
2532

2533
        shape_literal = ", ".join(type_info.dimensions)
×
2534
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
2535

2536
        if array_default.order_values is not None:
×
2537
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
2538
            arguments.append(f"order=[{order_literal}]")
×
2539

2540
        if array_default.pad_values is not None:
×
2541
            pad_elements = [
×
2542
                _format_scalar_default(element, type_info.kind, type_info.element_category)
2543
                for element in array_default.pad_values
2544
            ]
2545
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
2546

2547
        return f"reshape({', '.join(arguments)})"
×
2548
    return _format_scalar_default(value, type_info.kind, type_info.category)
1✔
2549

2550

2551
def _format_scalar_default(value: Any, kind: str | None, category: str | None) -> str:
1✔
2552
    if category == "integer":
1✔
2553
        if not isinstance(value, int):
1✔
2554
            raise ValueError("integer default must be an int")
×
2555
        suffix = f"_{kind}" if kind else ""
1✔
2556
        return f"{value}{suffix}"
1✔
2557
    if category == "real":
1✔
2558
        number = float(value)
1✔
2559
        literal = repr(number)
1✔
2560
        if literal.lower() == "nan":
1✔
2561
            raise ValueError("NaN defaults are not supported")
×
2562
        if "E" in literal:
1✔
2563
            literal = literal.replace("E", "e")
×
2564
        if "." not in literal and "e" not in literal:
1✔
2565
            literal = f"{literal}.0"
×
2566
        suffix = f"_{kind}" if kind else ""
1✔
2567
        return f"{literal}{suffix}"
1✔
2568
    if category == "boolean":
1✔
2569
        if not isinstance(value, bool):
1✔
2570
            raise ValueError("boolean default must be a bool")
×
2571
        return ".true." if value else ".false."
1✔
2572
    if category == "string":
1✔
2573
        if not isinstance(value, str):
1✔
2574
            raise ValueError("string default must be a str")
×
2575
        escaped = value.replace('"', '""')
1✔
2576
        return f'"{escaped}"'
1✔
2577
    raise ValueError(f"unsupported default category '{category}'")
×
2578

2579

2580
def _parse_default_dimensions(
1✔
2581
    dimensions: list[str],
2582
    constants: dict[str, int] | None,
2583
) -> list[int]:
2584
    if not dimensions:
1✔
2585
        raise ValueError("array property missing dimensions")
×
2586
    parsed: list[int] = []
1✔
2587
    for dim in dimensions:
1✔
2588
        if dim == ":":
1✔
2589
            raise ValueError("defaults not supported for deferred-size dimensions")
×
2590
        try:
1✔
2591
            parsed.append(int(dim))
1✔
2592
        except (TypeError, ValueError) as err:  # pragma: no cover - defensive
2593
            dim_key = dim.lower()
2594
            if constants is None or dim_key not in constants:
2595
                raise ValueError(
2596
                    "array default dimensions must be integer literals or defined constants"
2597
                ) from err
2598
            value = constants[dim_key]
2599
            if isinstance(value, bool) or not isinstance(value, int):
2600
                raise ValueError("array default dimension constants must be integers") from err
2601
            parsed.append(value)
2602
    return parsed
1✔
2603

2604

2605
def _prepare_array_default(
1✔
2606
    value: list[Any],
2607
    dims: list[int],
2608
    prop: dict[str, Any],
2609
) -> ArrayDefaultSpec:
2610
    default_values = _ensure_flat_scalar_list(value, "array default")
1✔
2611
    if not default_values:
1✔
2612
        raise ValueError("array default must contain at least one value")
×
2613

2614
    total_size = math.prod(dims)
1✔
2615
    if len(default_values) > total_size:
1✔
2616
        raise ValueError("array default longer than declared x-fortran-shape")
×
2617

2618
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
2619
    if not isinstance(order_raw, str):
1✔
2620
        raise ValueError("array default order must be 'F' or 'C'")
×
2621
    order = order_raw.upper()
1✔
2622
    if order not in {"F", "C"}:
1✔
2623
        raise ValueError("array default order must be 'F' or 'C'")
×
2624

2625
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
2626
    if not isinstance(repeat_raw, bool):
1✔
2627
        raise ValueError("array default repeat must be a boolean")
×
2628
    repeat = bool(repeat_raw)
1✔
2629

2630
    pad_raw = prop.get("x-fortran-default-pad")
1✔
2631
    pad_values: list[Any] | None = None
1✔
2632
    if pad_raw is not None:
1✔
2633
        if repeat:
1✔
2634
            raise ValueError("array default cannot set both pad and repeat")
×
2635
        if not isinstance(pad_raw, list):
1✔
2636
            pad_raw = [pad_raw]
1✔
2637
        pad_values = _ensure_flat_scalar_list(pad_raw, "array default pad")
1✔
2638
        if not pad_values:
1✔
2639
            raise ValueError("array default pad must contain at least one value")
×
2640

2641
    if len(default_values) < total_size and pad_values is None and not repeat:
1✔
2642
        raise ValueError(
×
2643
            "array default shorter than declared x-fortran-shape without pad or repeat"
2644
        )
2645

2646
    if repeat:
1✔
2647
        pad_values = list(default_values)
1✔
2648
        if not pad_values:
1✔
2649
            raise ValueError("array default repeat requires at least one value")
×
2650

2651
    order_values: list[int] | None = None
1✔
2652
    if order == "C" and len(dims) > 1:
1✔
2653
        rank = len(dims)
1✔
2654
        order_values = list(range(rank, 0, -1))
1✔
2655

2656
    return ArrayDefaultSpec(
1✔
2657
        source_values=list(default_values),
2658
        pad_values=pad_values,
2659
        order_values=order_values,
2660
    )
2661

2662

2663
def _ensure_flat_scalar_list(values: list[Any], description: str) -> list[Any]:
1✔
2664
    normalized: list[Any] = []
1✔
2665
    for element in values:
1✔
2666
        if isinstance(element, list):
1✔
2667
            raise ValueError(f"{description} must be a flat list")
×
2668
        normalized.append(element)
1✔
2669
    return normalized
1✔
2670

2671

2672
def _enum_values(
1✔
2673
    prop: dict[str, Any],
2674
    type_info: FieldTypeInfo,
2675
    constants: dict[str, int] | None,
2676
) -> list[Any] | None:
2677
    if type_info.category == "array":
1✔
2678
        enum_raw = _array_items_enum(prop)
1✔
2679
    else:
2680
        enum_raw = prop.get("enum")
1✔
2681
    if enum_raw is None:
1✔
2682
        return None
1✔
2683
    if not isinstance(enum_raw, list) or not enum_raw:
1✔
2684
        raise ValueError("property enum must be a non-empty list")
×
2685
    enum_values = _ensure_flat_scalar_list(enum_raw, "enum")
1✔
2686
    category = _enum_category(type_info)
1✔
2687
    if category not in {"integer", "string"}:
1✔
2688
        raise ValueError("enum only supported for integer or string values")
×
2689
    for value in enum_values:
1✔
2690
        _validate_enum_scalar(value, category, "enum")
1✔
2691
    _validate_enum_defaults(prop, type_info, enum_values, category, constants)
1✔
2692
    _validate_enum_examples(prop, type_info, enum_values, category)
1✔
2693
    return enum_values
1✔
2694

2695

2696
def _bounds_spec(
1✔
2697
    prop: dict[str, Any],
2698
    type_info: FieldTypeInfo,
2699
) -> dict[str, Any] | None:
2700
    if type_info.category == "array":
1✔
2701
        bounds_prop = _array_items_bounds(prop)
1✔
2702
        category = type_info.element_category
1✔
2703
    else:
2704
        bounds_prop = prop
1✔
2705
        category = type_info.category
1✔
2706

2707
    min_value, min_exclusive = _extract_bound_value(
1✔
2708
        bounds_prop, "minimum", "exclusiveMinimum"
2709
    )
2710
    max_value, max_exclusive = _extract_bound_value(
1✔
2711
        bounds_prop, "maximum", "exclusiveMaximum"
2712
    )
2713

2714
    if min_value is None and max_value is None:
1✔
2715
        return None
1✔
2716

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

2720
    if min_value is not None:
1✔
2721
        _validate_bound_scalar(min_value, category, "minimum")
1✔
2722
    if max_value is not None:
1✔
2723
        _validate_bound_scalar(max_value, category, "maximum")
1✔
2724

2725
    if min_value is not None and max_value is not None:
1✔
2726
        min_comp = float(min_value) if category == "real" else int(min_value)
1✔
2727
        max_comp = float(max_value) if category == "real" else int(max_value)
1✔
2728
        if min_exclusive or max_exclusive:
1✔
2729
            if min_comp >= max_comp:
1✔
2730
                raise ValueError("minimum must be less than maximum for exclusive bounds")
×
2731
        else:
2732
            if min_comp > max_comp:
×
2733
                raise ValueError("minimum must be <= maximum")
×
2734

2735
    return {
1✔
2736
        "min_value": min_value,
2737
        "min_exclusive": min_exclusive,
2738
        "max_value": max_value,
2739
        "max_exclusive": max_exclusive,
2740
        "category": category,
2741
    }
2742

2743

2744
def _extract_bound_value(
1✔
2745
    prop: dict[str, Any],
2746
    inclusive_key: str,
2747
    exclusive_key: str,
2748
) -> tuple[Any | None, bool]:
2749
    has_inclusive = inclusive_key in prop
1✔
2750
    has_exclusive = exclusive_key in prop
1✔
2751
    if has_inclusive and has_exclusive:
1✔
2752
        raise ValueError(
×
2753
            f"property must not define both '{inclusive_key}' and '{exclusive_key}'"
2754
        )
2755
    if has_exclusive:
1✔
2756
        value = prop.get(exclusive_key)
1✔
2757
        if value is None:
1✔
2758
            raise ValueError(f"{exclusive_key} must be a number")
×
2759
        return value, True
1✔
2760
    if has_inclusive:
1✔
2761
        value = prop.get(inclusive_key)
1✔
2762
        if value is None:
1✔
2763
            raise ValueError(f"{inclusive_key} must be a number")
×
2764
        return value, False
1✔
2765
    return None, False
1✔
2766

2767

2768
def _validate_bound_scalar(value: Any, category: str, label: str) -> None:
1✔
2769
    if category == "integer":
1✔
2770
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2771
            raise ValueError(f"{label} must be an integer")
×
2772
        return
1✔
2773
    if category == "real":
1✔
2774
        if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
2775
            raise ValueError(f"{label} must be a number")
×
2776
        if math.isinf(float(value)):
1✔
2777
            raise ValueError(f"{label} must not be infinite")
×
2778
        if math.isnan(float(value)):
1✔
2779
            raise ValueError(f"{label} must not be NaN")
×
2780
        return
1✔
2781
    raise ValueError("bounds only supported for integer or real values")
×
2782

2783

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

2788
    default_defined = "default" in prop
1✔
2789
    items_default = _array_items_default(prop)
1✔
2790
    items_defined = "default" in items_default
1✔
2791
    items_value = items_default.get("default") if items_defined else None
1✔
2792

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

2796
    if items_defined:
1✔
2797
        for key in ("x-fortran-default-order", "x-fortran-default-repeat", "x-fortran-default-pad"):
1✔
2798
            if key in prop:
1✔
2799
                raise ValueError("array items default must not use x-fortran-default-* options")
×
2800
        if isinstance(items_value, list):
1✔
2801
            raise ValueError("array items default must be a scalar")
×
2802
        return items_value, True
1✔
2803

2804
    if default_defined:
1✔
2805
        default_value = prop.get("default")
1✔
2806
        if not isinstance(default_value, list):
1✔
2807
            raise ValueError("array default must be a list")
1✔
2808
        return default_value, False
1✔
2809
    return None
1✔
2810

2811

2812
def _array_items_enum(prop: dict[str, Any]) -> list[Any] | None:
1✔
2813
    current = prop
1✔
2814
    while current.get("type") == "array":
1✔
2815
        if "enum" in current:
1✔
2816
            raise ValueError("array enum must be defined on items")
1✔
2817
        items = current.get("items")
1✔
2818
        if not isinstance(items, dict):
1✔
2819
            raise ValueError("array property must define 'items'")
×
2820
        current = items
1✔
2821
    return current.get("enum")
1✔
2822

2823

2824
def _array_items_default(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2825
    current = prop
1✔
2826
    while current.get("type") == "array":
1✔
2827
        items = current.get("items")
1✔
2828
        if not isinstance(items, dict):
1✔
2829
            raise ValueError("array property must define 'items'")
×
2830
        if items.get("type") == "array":
1✔
2831
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
1✔
2832
        current = items
1✔
2833
    return current
1✔
2834

2835

2836
def _array_items_bounds(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2837
    current = prop
1✔
2838
    while current.get("type") == "array":
1✔
2839
        for key in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"):
1✔
2840
            if key in current:
1✔
2841
                raise ValueError("array bounds must be defined on items")
×
2842
        items = current.get("items")
1✔
2843
        if not isinstance(items, dict):
1✔
2844
            raise ValueError("array property must define 'items'")
×
2845
        if items.get("type") == "array":
1✔
2846
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2847
        current = items
1✔
2848
    return current
1✔
2849

2850

2851
def _validate_enum_scalar(value: Any, category: str, label: str) -> None:
1✔
2852
    if category == "integer":
1✔
2853
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2854
            raise ValueError(f"{label} values must be integers")
×
2855
        return
1✔
2856
    if category == "string":
1✔
2857
        if not isinstance(value, str):
1✔
2858
            raise ValueError(f"{label} values must be strings")
×
2859
        return
1✔
2860
    raise ValueError("enum only supported for integer or string values")
×
2861

2862

2863
def _ensure_enum_member(value: Any, enum_values: list[Any], category: str, label: str) -> None:
1✔
2864
    _validate_enum_scalar(value, category, label)
1✔
2865
    if value not in enum_values:
1✔
2866
        raise ValueError(f"{label} value must be one of enum values")
×
2867

2868

2869
def _validate_enum_defaults(
1✔
2870
    prop: dict[str, Any],
2871
    type_info: FieldTypeInfo,
2872
    enum_values: list[Any],
2873
    category: str,
2874
    constants: dict[str, int] | None,
2875
) -> None:
2876
    if type_info.category == "array":
1✔
2877
        default_info = _array_default_value(prop)
1✔
2878
        if default_info is not None:
1✔
2879
            default_value, default_from_items = default_info
×
2880
            if default_from_items:
×
2881
                _ensure_enum_member(default_value, enum_values, category, "default")
×
2882
            else:
2883
                default_values = _ensure_flat_scalar_list(default_value, "array default")
×
2884
                for value in default_values:
×
2885
                    _ensure_enum_member(value, enum_values, category, "default")
×
2886
        pad_raw = prop.get("x-fortran-default-pad")
1✔
2887
        if pad_raw is not None:
1✔
2888
            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
2889
            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
×
2890
            for value in pad_values:
×
2891
                _ensure_enum_member(value, enum_values, category, "pad")
×
2892
        return
1✔
2893
    if "default" not in prop:
1✔
2894
        return
1✔
2895
    default_value = prop["default"]
1✔
2896
    if isinstance(default_value, list):
1✔
2897
        raise ValueError("scalar default must not be a list")
×
2898
    _ensure_enum_member(default_value, enum_values, category, "default")
1✔
2899

2900

2901
def _validate_enum_examples(
1✔
2902
    prop: dict[str, Any],
2903
    type_info: FieldTypeInfo,
2904
    enum_values: list[Any],
2905
    category: str,
2906
) -> None:
2907
    examples = prop.get("examples")
1✔
2908
    if examples is None:
1✔
2909
        return
1✔
2910
    if not isinstance(examples, list):
×
2911
        raise ValueError("property examples must be a list")
×
2912
    for example in examples:
×
2913
        if type_info.category == "array":
×
2914
            if isinstance(example, list):
×
2915
                values = _ensure_flat_scalar_list(example, "array examples")
×
2916
                for value in values:
×
2917
                    _ensure_enum_member(value, enum_values, category, "example")
×
2918
            else:
2919
                _ensure_enum_member(example, enum_values, category, "example")
×
2920
        else:
2921
            if isinstance(example, list):
×
2922
                raise ValueError("scalar examples must not be lists")
×
2923
            _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