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

MuellerSeb / nml-tools / 26169996067

20 May 2026 02:41PM UTC coverage: 79.35% (+5.0%) from 74.315%
26169996067

Pull #27

github

MuellerSeb
Align integer-only constant API typing
Pull Request #27: Add Runtime Array Dimensions

466 of 520 new or added lines in 7 files covered. (89.62%)

4 existing lines in 2 files now uncovered.

2663 of 3356 relevant lines covered (79.35%)

0.79 hits per line

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

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

3
from __future__ import annotations
1✔
4

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

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

13
from ._utils import strip_trailing_whitespace
1✔
14

15
_TEMPLATE_ENV = Environment(
1✔
16
    loader=FileSystemLoader(Path(__file__).resolve().parent / "templates"),
17
    trim_blocks=True,
18
    lstrip_blocks=False,
19
    keep_trailing_newline=True,
20
    undefined=StrictUndefined,
21
)
22

23

24
@dataclass
1✔
25
class ScalarTypeInfo:
1✔
26
    """Information about a scalar Fortran type."""
27

28
    type_spec: str
1✔
29
    arg_type_spec: str
1✔
30
    kind: str | None
1✔
31
    category: str
1✔
32
    length_expr: str | None = None
1✔
33

34

35
@dataclass
1✔
36
class FieldTypeInfo:
1✔
37
    """Information about a field (scalar or array) Fortran type."""
38

39
    type_spec: str
1✔
40
    arg_type_spec: str
1✔
41
    dimensions: list[str]
1✔
42
    kind: str | None
1✔
43
    category: str
1✔
44
    length_expr: str | None = None
1✔
45
    element_category: str | None = None
1✔
46

47

48
@dataclass
1✔
49
class FieldSpec:
1✔
50
    """Information required to render a schema property."""
51

52
    order: int
1✔
53
    name: str
1✔
54
    title: str
1✔
55
    description: str | None
1✔
56
    declaration: str
1✔
57
    local_declaration: str
1✔
58
    required: bool
1✔
59
    sentinel_assignment: str | None
1✔
60
    sentinel_check: str | None
1✔
61
    default_assignment: str | None
1✔
62
    set_default_assignment: str | None
1✔
63
    set_present_assignment: str | None
1✔
64
    argument_declaration: str
1✔
65
    type_category: str
1✔
66
    runtime_sized_array: bool = False
1✔
67
    rank: int = 0
1✔
68

69

70
@dataclass
1✔
71
class ArrayDefaultSpec:
1✔
72
    """Normalized representation of an array default value."""
73

74
    source_values: list[Any]
1✔
75
    pad_values: list[Any] | None
1✔
76
    order_values: list[int] | None
1✔
77

78

79
@dataclass
1✔
80
class ConstantSpec:
1✔
81
    """Constant definition for helper modules."""
82

83
    name: str
1✔
84
    type_spec: str
1✔
85
    value: str
1✔
86
    doc: str | None
1✔
87

88

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

119

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

149

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

172

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

198

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

219
    if schema.get("type") != "object":
1✔
220
        raise ValueError("schema root must be of type 'object'")
×
221

222
    properties = schema.get("properties")
1✔
223
    if not isinstance(properties, dict) or not properties:
1✔
224
        raise ValueError("schema must define object 'properties'")
×
225

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

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

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

317
    def _add_helper_import(name: str) -> None:
1✔
318
        if not any(existing.lower() == name.lower() for existing in helper_imports):
1✔
319
            helper_imports.append(name)
1✔
320

321
    def _alias_runtime_default(expr: str) -> str:
1✔
322
        aliased = expr
1✔
323
        for const_name, default_name in runtime_dimension_defaults.items():
1✔
324
            aliased = re.sub(rf"\b{re.escape(const_name)}\b", default_name, aliased)
1✔
325
        return aliased
1✔
326

327
    def _register_runtime_dimension(dim_name: str) -> str:
1✔
328
        local_name = runtime_dimension_locals.get(dim_name)
1✔
329
        if local_name is None:
1✔
330
            local_name = f"dim_{dim_name}"
1✔
331
            runtime_dimension_locals[dim_name] = local_name
1✔
332
            default_name = f"{dim_name}_default"
1✔
333
            runtime_dimension_defaults[dim_name] = default_name
1✔
334
            _add_helper_import(f"{default_name}=>{dim_name}")
1✔
335
            runtime_dimensions.append(
1✔
336
                {
337
                    "name": dim_name,
338
                    "default_name": default_name,
339
                    "local_name": local_name,
340
                }
341
            )
342
        return local_name
1✔
343

344
    current_property: str | None = None
1✔
345
    try:
1✔
346
        for index, (display_name, attr_name, prop) in enumerate(property_items):
1✔
347
            current_property = display_name
1✔
348
            name = attr_name
1✔
349
            _reject_runtime_dimension_lengths(prop, runtime_dimension_values)
1✔
350
            type_info = _field_type_info(prop, static_constants)
1✔
351
            for const_name in _collect_dimension_constants(type_info.dimensions, shape_constants):
1✔
352
                const_key = const_name.lower()
1✔
353
                if const_key in runtime_dimension_values:
1✔
354
                    _register_runtime_dimension(const_key)
1✔
355
                else:
356
                    _add_helper_import(const_name)
1✔
357
            if type_info.length_expr and not _is_int_literal(type_info.length_expr):
1✔
358
                _add_helper_import(type_info.length_expr)
1✔
359
            if type_info.kind:
1✔
360
                kind_ids.append(type_info.kind)
1✔
361

362
            runtime_shape = list(type_info.dimensions)
1✔
363
            dynamic_shape = False
1✔
364
            if type_info.category == "array":
1✔
365
                for dim_index, dim in enumerate(runtime_shape):
1✔
366
                    if dim == ":" or _is_int_literal(dim):
1✔
367
                        continue
1✔
368
                    if not _FORTRAN_IDENTIFIER.match(dim):
1✔
NEW
369
                        raise ValueError(
×
370
                            "array property 'x-fortran-shape' entries must be ints or identifiers"
371
                        )
372
                    dim_key = dim.lower()
1✔
373
                    if dim_key in runtime_dimension_values:
1✔
374
                        local_dim_name = _register_runtime_dimension(dim_key)
1✔
375
                        runtime_shape[dim_index] = f"this%{local_dim_name}"
1✔
376
                        dynamic_shape = True
1✔
377
                    elif dim_key not in static_constants:
1✔
NEW
378
                        raise ValueError(f"dimension constant '{dim}' is not defined in config")
×
379

380
            runtime_length_expr = type_info.length_expr
1✔
381
            if (
1✔
382
                type_info.length_expr
383
                and not _is_int_literal(type_info.length_expr)
384
                and type_info.length_expr.lower() in runtime_dimension_values
385
            ):
NEW
386
                raise ValueError(
×
387
                    f"dimension '{type_info.length_expr}' cannot be used as x-fortran-len"
388
                )
389
            type_spec_with_defaults = _alias_runtime_default(type_info.type_spec)
1✔
390

391
            array_default_info: tuple[Any, bool] | None = None
1✔
392
            if type_info.category == "array":
1✔
393
                array_default_info = _array_default_value(prop)
1✔
394

395
            flex_dim = _parse_flex_dim(prop, type_info)
1✔
396
            if flex_dim > 0:
1✔
397
                if type_info.element_category == "boolean":
1✔
398
                    raise ValueError("flex arrays cannot use boolean elements")
1✔
399
                if array_default_info is not None or any(
1✔
400
                    key in prop
401
                    for key in (
402
                        "x-fortran-default-order",
403
                        "x-fortran-default-repeat",
404
                        "x-fortran-default-pad",
405
                    )
406
                ):
407
                    raise ValueError("flex arrays cannot define defaults")
1✔
408

409
            is_runtime_sized = (
1✔
410
                type_info.category == "array" and dynamic_shape
411
            )
412

413
            if is_runtime_sized:
1✔
414
                declaration = _render_runtime_declaration(
1✔
415
                    type_info,
416
                    name,
417
                )
418
                local_decl = _render_runtime_local_declaration(
1✔
419
                    type_info,
420
                    name,
421
                    runtime_dimensions=runtime_shape,
422
                )
423
                runtime_allocations.extend(
1✔
424
                    _render_runtime_allocations(
425
                        type_info,
426
                        name,
427
                        runtime_dimensions=runtime_shape,
428
                        runtime_length_expr=runtime_length_expr,
429
                        target_prefix="this%",
430
                    )
431
                )
432
                runtime_deallocations.extend(
1✔
433
                    _render_runtime_deallocations(name, target_prefix="this%")
434
                )
435
                runtime_local_allocations.extend(
1✔
436
                    _render_runtime_local_allocations(
437
                        type_info,
438
                        name,
439
                        runtime_dimensions=runtime_shape,
440
                        runtime_length_expr=runtime_length_expr,
441
                    )
442
                )
443
            else:
444
                declaration = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
445
                local_decl = _render_declaration(type_info.type_spec, type_info.dimensions, name)
1✔
446

447
            title_raw = prop.get("title")
1✔
448
            if title_raw is None:
1✔
449
                title = display_name
1✔
450
            elif not isinstance(title_raw, str):
1✔
451
                raise ValueError(f"property '{display_name}' title must be a string")
×
452
            else:
453
                title = title_raw.strip() or name
1✔
454
            description = prop.get("description")
1✔
455
            declaration_with_doc = f"{declaration} !< {title}"
1✔
456

457
            dynamic_array = type_info.category == "array" and is_runtime_sized
1✔
458

459
            is_required = name in required_set
1✔
460
            if type_info.category == "array":
1✔
461
                has_default = array_default_info is not None
1✔
462
            else:
463
                has_default = "default" in prop
1✔
464

465
            default_from_items = False
1✔
466
            default_values: list[Any] | None = None
1✔
467
            parsed_dims: list[int] | None = None
1✔
468
            array_default_spec: ArrayDefaultSpec | None = None
1✔
469

470
            if type_info.category == "array" and has_default:
1✔
471
                if array_default_info is None:
1✔
472
                    raise ValueError(f"missing array default for '{display_name}'")
×
473
                default_raw, default_from_items = array_default_info
1✔
474
                if default_from_items:
1✔
475
                    if isinstance(default_raw, list):
1✔
476
                        raise ValueError("array items default must be a scalar")
×
477
                    default_values = [default_raw]
1✔
478
                else:
479
                    if not isinstance(default_raw, list):
1✔
480
                        raise ValueError("array default must be a list")
×
481
                    default_values = default_raw
1✔
482
                    parsed_dims = _parse_default_dimensions(type_info.dimensions, shape_constants)
1✔
483
                    array_default_spec = _prepare_array_default(default_values, parsed_dims, prop)
1✔
484

485
            needs_sentinel = (not is_required) and (not has_default)
1✔
486
            requires_sentinel = is_required or needs_sentinel
1✔
487

488
            arg_dimensions = type_info.dimensions
1✔
489
            if type_info.category == "array" and (flex_dim > 0 or has_default or dynamic_array):
1✔
490
                arg_dimensions = [":" for _ in type_info.dimensions]
1✔
491
            argument_decl = _render_argument_declaration(
1✔
492
                name=name,
493
                type_info=type_info,
494
                is_required=is_required,
495
                dimensions=arg_dimensions,
496
                doc=title,
497
            )
498
            local_init_assignments.append(f"{name} = this%{name}")
1✔
499

500
            sentinel_assignment: str | None = None
1✔
501
            sentinel_condition: str | None = None
1✔
502
            set_sentinel_condition: str | None = None
1✔
503
            if requires_sentinel:
1✔
504
                if is_required and type_info.category == "boolean":
1✔
505
                    raise ValueError(
×
506
                        f"required {type_info.category} '{display_name}' is not supported"
507
                    )
508
                if is_required and type_info.category == "array":
1✔
509
                    if type_info.element_category == "boolean":
1✔
510
                        raise ValueError("required boolean arrays are not supported")
×
511
                if needs_sentinel and type_info.category == "boolean":
1✔
512
                    raise ValueError(f"optional boolean '{display_name}' must define a default")
×
513
                if needs_sentinel and type_info.category == "array":
1✔
514
                    if type_info.element_category == "boolean":
1✔
515
                        raise ValueError("optional boolean arrays must define a default")
×
516
                value_expr, condition_expr, uses_ieee = _sentinel_expressions(
1✔
517
                    type_info,
518
                    var_ref=f"this%{name}",
519
                )
520
                sentinel_assignment = _render_sentinel_assignment(
1✔
521
                    type_info,
522
                    target_ref=f"this%{name}",
523
                    value_expr=value_expr,
524
                    comment=_sentinel_comment(type_info, required=is_required),
525
                )
526
                sentinel_condition = condition_expr
1✔
527
                sentinel_assignments.append(sentinel_assignment)
1✔
528
                if uses_ieee:
1✔
529
                    requires_ieee = True
1✔
530
                if not has_default and type_info.category != "boolean":
1✔
531
                    set_value_expr, set_condition_expr, set_uses_ieee = _sentinel_expressions(
1✔
532
                        type_info,
533
                        var_ref=f"this%{name}",
534
                    )
535
                    if set_uses_ieee:
1✔
536
                        requires_ieee = True
1✔
537
                    set_sentinel_condition = set_condition_expr
1✔
538
                    if needs_sentinel:
1✔
539
                        sent_com = _sentinel_comment(type_info, required=False)
1✔
540
                        set_optional_defaults.append(
1✔
541
                            _render_sentinel_assignment(
542
                                type_info,
543
                                target_ref=f"this%{name}",
544
                                value_expr=set_value_expr,
545
                                comment=sent_com,
546
                            )
547
                        )
548

549
            if is_required and type_info.category != "array":
1✔
550
                required_scalar_names.add(name)
1✔
551

552
            if is_required and type_info.category == "array" and flex_dim == 0:
1✔
553
                element_category = type_info.element_category
1✔
554
                if element_category is None:
1✔
555
                    raise ValueError("array field missing element category")
×
556
                all_missing, any_missing, uses_ieee = _array_missing_conditions(
1✔
557
                    element_category,
558
                    var_ref=_array_section_ref(f"this%{name}", len(type_info.dimensions)),
559
                    len_ref=f"this%{name}",
560
                )
561
                required_array_by_name[name] = {
1✔
562
                    "name": display_name,
563
                    "attr_name": name,
564
                    "runtime_array": dynamic_array,
565
                    "all_missing_condition": all_missing,
566
                    "any_missing_condition": any_missing,
567
                }
568
                uses_partly_set = True
1✔
569
                if uses_ieee:
1✔
570
                    requires_ieee = True
1✔
571

572
            flex_bounds: list[dict[str, Any]] | None = None
1✔
573
            partial_bounds: list[dict[str, Any]] | None = None
1✔
574
            if flex_dim > 0:
1✔
575
                rank = len(type_info.dimensions)
1✔
576
                element_category = type_info.element_category
1✔
577
                if element_category is None:
1✔
578
                    raise ValueError("array field missing element category")
×
579
                flex_dims: list[int] = list(range(rank - flex_dim + 1, rank + 1))
1✔
580
                slice_missing_conditions: list[str] = []
1✔
581
                slice_uses_ieee = False
1✔
582
                bounds: list[dict[str, Any]] = []
1✔
583
                lb_vars: dict[int, str] = {}
1✔
584
                ub_vars: dict[int, str] = {}
1✔
585
                for flex_dim_index in flex_dims:
1✔
586
                    lb_var, ub_var = _flex_bound_vars(flex_dim_index)
1✔
587
                    lb_vars[flex_dim_index] = lb_var
1✔
588
                    ub_vars[flex_dim_index] = ub_var
1✔
589
                    bounds.append({"dim": flex_dim_index, "lb_var": lb_var, "ub_var": ub_var})
1✔
590
                    flex_bound_vars.add(lb_var)
1✔
591
                    flex_bound_vars.add(ub_var)
1✔
592
                    slice_ref = _slice_ref(name, rank, flex_dim_index, "idx")
1✔
593
                    slice_missing_expr, uses_ieee = _element_missing_expression(
1✔
594
                        element_category,
595
                        var_ref=slice_ref,
596
                        len_ref=f"this%{name}",
597
                    )
598
                    slice_missing_conditions.append(
1✔
599
                        f"all({slice_missing_expr})" if rank > 1 else slice_missing_expr
600
                    )
601
                    slice_uses_ieee = slice_uses_ieee or uses_ieee
1✔
602
                prefix_ref = _slice_ref_bounds(name, rank, flex_dims, lb_vars, ub_vars)
1✔
603
                prefix_missing_expr, uses_ieee_prefix = _element_missing_expression(
1✔
604
                    element_category,
605
                    var_ref=prefix_ref,
606
                    len_ref=f"this%{name}",
607
                )
608
                prefix_any_missing_condition = f"any({prefix_missing_expr})"
1✔
609
                flex_bounds = bounds
1✔
610
                flex_arrays.append(
1✔
611
                    {
612
                        "name": name,
613
                        "display_name": display_name,
614
                        "rank": rank,
615
                        "flex_dims": flex_dims,
616
                        "required": is_required,
617
                        "runtime_array": dynamic_array,
618
                        "bounds": bounds,
619
                        "slice_missing_conditions": slice_missing_conditions,
620
                        "prefix_any_missing_condition": prefix_any_missing_condition,
621
                    }
622
                )
623
                uses_partly_set = True
1✔
624
                if slice_uses_ieee or uses_ieee_prefix:
1✔
625
                    requires_ieee = True
×
626
            elif type_info.category == "array" and has_default:
1✔
627
                rank = len(type_info.dimensions)
1✔
628
                bounds = []
1✔
629
                for array_dim_index in range(1, rank + 1):
1✔
630
                    lb_var, ub_var = _flex_bound_vars(array_dim_index)
1✔
631
                    bounds.append({"dim": array_dim_index, "lb_var": lb_var, "ub_var": ub_var})
1✔
632
                    flex_bound_vars.add(lb_var)
1✔
633
                    flex_bound_vars.add(ub_var)
1✔
634
                partial_bounds = bounds
1✔
635

636
            default_assignment: str | None = None
1✔
637
            set_default_assignment: str | None = None
1✔
638
            if has_default and is_required:
1✔
639
                raise ValueError(f"required property '{display_name}' cannot define a default")
×
640
            if has_default:
1✔
641
                default_const_name = f"{name}_default"
1✔
642
                if type_info.category == "array":
1✔
643
                    if default_values is None:
1✔
644
                        raise ValueError(f"missing array default for '{display_name}'")
×
645
                    default_is_scalar = default_from_items
1✔
646

647
                    repeat = False
1✔
648
                    pad_raw = None
1✔
649
                    pad_const_name: str | None = None
1✔
650
                    pad_is_scalar = False
1✔
651

652
                    if not default_from_items:
1✔
653
                        repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
654
                        if not isinstance(repeat_raw, bool):
1✔
655
                            raise ValueError("array default repeat must be a boolean")
×
656
                        repeat = bool(repeat_raw)
1✔
657
                        pad_raw = prop.get("x-fortran-default-pad")
1✔
658
                        if pad_raw is not None:
1✔
659
                            pad_const_name = f"{name}_pad"
1✔
660
                            pad_is_scalar = not isinstance(pad_raw, list)
1✔
661
                            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
1✔
662
                            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
1✔
663
                            pad_elements = [
1✔
664
                                _format_scalar_default(
665
                                    element, type_info.kind, type_info.element_category
666
                                )
667
                                for element in pad_values
668
                            ]
669
                            if pad_is_scalar:
1✔
670
                                pad_literal = _format_scalar_default(
1✔
671
                                    pad_raw, type_info.kind, type_info.element_category
672
                                )
673
                                default_parameters.append(
1✔
674
                                    f"{type_spec_with_defaults}, parameter, public :: "
675
                                    f"{pad_const_name} = {pad_literal}"
676
                                )
677
                            else:
678
                                default_parameters.append(
×
679
                                    f"{type_spec_with_defaults}, parameter, public :: "
680
                                    f"{pad_const_name}({len(pad_elements)}) = "
681
                                    f"[{', '.join(pad_elements)}]"
682
                                )
683

684
                        if parsed_dims is None:
1✔
NEW
685
                            parsed_dims = _parse_default_dimensions(
×
686
                                type_info.dimensions, shape_constants
687
                            )
688
                        if array_default_spec is None:
1✔
689
                            array_default_spec = _prepare_array_default(
×
690
                                default_values, parsed_dims, prop
691
                            )
692

693
                    if default_is_scalar:
1✔
694
                        default_literal = _format_scalar_default(
1✔
695
                            default_values[0], type_info.kind, type_info.element_category
696
                        )
697
                        default_parameters.append(
1✔
698
                            f"{type_spec_with_defaults}, parameter, public :: "
699
                            f"{default_const_name} = {default_literal}"
700
                        )
701
                    else:
702
                        if array_default_spec is None:
1✔
703
                            raise ValueError(
×
704
                                f"missing array default specification for '{display_name}'"
705
                            )
706
                        default_elements = [
1✔
707
                            _format_scalar_default(
708
                                element, type_info.kind, type_info.element_category
709
                            )
710
                            for element in array_default_spec.source_values
711
                        ]
712
                        default_parameters.append(
1✔
713
                            f"{type_spec_with_defaults}, parameter, public :: "
714
                            f"{default_const_name}({len(default_elements)}) = "
715
                            f"[{', '.join(default_elements)}]"
716
                        )
717

718
                    if default_from_items:
1✔
719
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
720
                    elif (
1✔
721
                        len(type_info.dimensions) == 1
722
                        and array_default_spec is not None
723
                        and array_default_spec.order_values is None
724
                        and array_default_spec.pad_values is None
725
                    ):
726
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
727
                    else:
728
                        source_expr = default_const_name
1✔
729
                        shape_expr = ", ".join(runtime_shape)
1✔
730
                        arguments = [source_expr, f"shape=[{shape_expr}]"]
1✔
731
                        if array_default_spec is not None:
1✔
732
                            if array_default_spec.order_values is not None:
1✔
733
                                order_literal = ", ".join(
1✔
734
                                    str(index) for index in array_default_spec.order_values
735
                                )
736
                                arguments.append(f"order=[{order_literal}]")
1✔
737
                            if array_default_spec.pad_values is not None:
1✔
738
                                if repeat:
1✔
739
                                    pad_expr = default_const_name
1✔
740
                                else:
741
                                    if pad_const_name is None:
1✔
742
                                        raise ValueError(
×
743
                                            f"missing pad values for array default '{display_name}'"
744
                                        )
745
                                    pad_expr = (
1✔
746
                                        pad_const_name
747
                                        if not pad_is_scalar
748
                                        else f"[{pad_const_name}]"
749
                                    )
750
                                arguments.append(f"pad={pad_expr}")
1✔
751
                        default_assignment = _format_reshape_assignment(name, arguments)
1✔
752
                    set_default_assignment = default_assignment
1✔
753
                else:
754
                    default_literal = _format_default(prop["default"], type_info, prop, constants)
1✔
755
                    default_parameters.append(
1✔
756
                        f"{type_spec_with_defaults}, parameter, public :: "
757
                        f"{default_const_name} = {default_literal}"
758
                    )
759
                    if type_info.category == "boolean":
1✔
760
                        default_assignment = (
1✔
761
                            f"this%{name} = {default_const_name} "
762
                            "! bool values always need a default"
763
                        )
764
                    else:
765
                        default_assignment = f"this%{name} = {default_const_name}"
1✔
766
                    set_default_assignment = f"this%{name} = {default_const_name}"
1✔
767

768
                if default_assignment is None or set_default_assignment is None:
1✔
769
                    raise ValueError(f"missing default assignment for '{display_name}'")
×
770
                default_assignments.append(default_assignment)
1✔
771
                set_optional_defaults.append(set_default_assignment)
1✔
772

773
            if type_info.category == "array" and any(
1✔
774
                (dim != ":") and (not _is_int_literal(dim)) for dim in type_info.dimensions
775
            ):
776
                required_default_elements: int | None = None
1✔
777
                if has_default and default_values is not None:
1✔
778
                    if default_from_items:
1✔
779
                        required_default_elements = 1
1✔
780
                    elif array_default_spec is not None:
1✔
781
                        required_default_elements = len(array_default_spec.source_values)
1✔
782
                    else:
NEW
783
                        required_default_elements = len(default_values)
×
784
                if required_default_elements is not None and required_default_elements > 1:
1✔
785
                    runtime_default_extent_requirements.append(
1✔
786
                        {
787
                            "field": display_name,
788
                            "dimensions": list(type_info.dimensions),
789
                            "required_elements": required_default_elements,
790
                        }
791
                    )
792

793
            enum_values = _enum_values(prop, type_info, constants)
1✔
794
            if enum_values is not None:
1✔
795
                enum_category = _enum_category(type_info)
1✔
796
                enum_const_name = f"{name}_enum_values"
1✔
797
                enum_literals = [
1✔
798
                    _format_scalar_default(value, type_info.kind, enum_category)
799
                    for value in enum_values
800
                ]
801
                if enum_category == "string":
1✔
802
                    enum_array_literal = (
1✔
803
                        f"[{type_spec_with_defaults} :: {', '.join(enum_literals)}]"
804
                    )
805
                else:
806
                    enum_array_literal = f"[{', '.join(enum_literals)}]"
1✔
807
                if enum_category == "string":
1✔
808
                    enum_parameters.append(
1✔
809
                        f"{type_spec_with_defaults}, parameter, public :: &\n"
810
                        f"    {enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
811
                    )
812
                else:
813
                    enum_parameters.append(
1✔
814
                        f"{type_spec_with_defaults}, parameter, public :: "
815
                        f"{enum_const_name}({len(enum_literals)}) = {enum_array_literal}"
816
                    )
817
                enum_type_info = (
1✔
818
                    _element_type_info(type_info) if type_info.category == "array" else type_info
819
                )
820
                _, missing_condition, _ = _sentinel_expressions(
1✔
821
                    enum_type_info,
822
                    var_ref="val",
823
                    len_ref="val",
824
                )
825
                enum_functions.append(
1✔
826
                    {
827
                        "name": name,
828
                        "func_name": f"{name}_in_enum",
829
                        "arg_type_spec": _enum_arg_type_spec(type_info),
830
                        "enum_values_name": enum_const_name,
831
                        "use_trim": enum_category == "string",
832
                        "missing_condition": missing_condition,
833
                    }
834
                )
835
                if type_info.category == "array":
1✔
836
                    enum_checks.append(
1✔
837
                        {
838
                            "name": name,
839
                            "display_name": display_name,
840
                            "func_name": f"{name}_in_enum",
841
                            "is_array": True,
842
                            "runtime_array": dynamic_array,
843
                            "array_ref": f"this%{name}",
844
                        }
845
                    )
846
                else:
847
                    enum_checks.append(
1✔
848
                        {
849
                            "name": name,
850
                            "display_name": display_name,
851
                            "func_name": f"{name}_in_enum",
852
                            "is_array": False,
853
                            "element_ref": f"this%{name}",
854
                        }
855
                    )
856

857
            bounds_spec = _bounds_spec(prop, type_info)
1✔
858
            if bounds_spec is not None:
1✔
859
                bounds_category = bounds_spec["category"]
1✔
860
                bounds_type_info = (
1✔
861
                    _element_type_info(type_info) if type_info.category == "array" else type_info
862
                )
863
                min_value = bounds_spec["min_value"]
1✔
864
                max_value = bounds_spec["max_value"]
1✔
865
                min_exclusive = bounds_spec["min_exclusive"]
1✔
866
                max_exclusive = bounds_spec["max_exclusive"]
1✔
867
                min_name = None
1✔
868
                max_name = None
1✔
869
                if min_value is not None:
1✔
870
                    min_name = f"{name}_min_excl" if min_exclusive else f"{name}_min"
1✔
871
                    min_literal = _format_scalar_default(
1✔
872
                        min_value, bounds_type_info.kind, bounds_category
873
                    )
874
                    bounds_type_spec_with_defaults = _alias_runtime_default(
1✔
875
                        bounds_type_info.type_spec
876
                    )
877
                    bounds_parameters.append(
1✔
878
                        f"{bounds_type_spec_with_defaults}, parameter, public :: "
879
                        f"{min_name} = {min_literal}"
880
                    )
881
                if max_value is not None:
1✔
882
                    max_name = f"{name}_max_excl" if max_exclusive else f"{name}_max"
1✔
883
                    max_literal = _format_scalar_default(
1✔
884
                        max_value, bounds_type_info.kind, bounds_category
885
                    )
886
                    bounds_type_spec_with_defaults = _alias_runtime_default(
1✔
887
                        bounds_type_info.type_spec
888
                    )
889
                    bounds_parameters.append(
1✔
890
                        f"{bounds_type_spec_with_defaults}, parameter, public :: "
891
                        f"{max_name} = {max_literal}"
892
                    )
893
                _, missing_condition, uses_ieee = _sentinel_expressions(
1✔
894
                    bounds_type_info,
895
                    var_ref="val",
896
                    len_ref="val",
897
                )
898
                if uses_ieee:
1✔
899
                    requires_ieee = True
1✔
900
                bounds_functions.append(
1✔
901
                    {
902
                        "name": name,
903
                        "func_name": f"{name}_in_bounds",
904
                        "arg_type_spec": bounds_type_info.arg_type_spec,
905
                        "has_min": min_value is not None,
906
                        "has_max": max_value is not None,
907
                        "min_name": min_name,
908
                        "max_name": max_name,
909
                        "min_exclusive": min_exclusive,
910
                        "max_exclusive": max_exclusive,
911
                        "missing_condition": missing_condition,
912
                    }
913
                )
914
                if type_info.category == "array":
1✔
915
                    bounds_checks.append(
1✔
916
                        {
917
                            "name": name,
918
                            "display_name": display_name,
919
                            "func_name": f"{name}_in_bounds",
920
                            "is_array": True,
921
                            "runtime_array": dynamic_array,
922
                            "array_ref": f"this%{name}",
923
                        }
924
                    )
925
                else:
926
                    bounds_checks.append(
1✔
927
                        {
928
                            "name": name,
929
                            "display_name": display_name,
930
                            "func_name": f"{name}_in_bounds",
931
                            "is_array": False,
932
                            "element_ref": f"this%{name}",
933
                        }
934
                    )
935

936
            is_array = type_info.category == "array"
1✔
937
            if is_required:
1✔
938
                if is_array and flex_dim > 0:
1✔
939
                    set_required_assignments.append(
1✔
940
                        _render_flex_set_block(
941
                            name,
942
                            len(type_info.dimensions),
943
                            flex_dim,
944
                            flex_bounds or [],
945
                        )
946
                    )
947
                elif is_array and has_default:
1✔
948
                    # Preserve partial-update semantics for arrays with defaults,
949
                    # including runtime-sized arrays driven by constants.
UNCOV
950
                    set_required_assignments.append(
×
951
                        _render_partial_set_block(
952
                            name,
953
                            len(type_info.dimensions),
954
                            partial_bounds or [],
955
                        )
956
                    )
957
                elif is_array and dynamic_array:
1✔
958
                    set_required_assignments.append(
1✔
959
                        _render_exact_set_block(
960
                            name,
961
                            len(type_info.dimensions),
962
                        )
963
                    )
964
                else:
965
                    set_required_assignments.append(f"this%{name} = {name}")
1✔
966

967
            if not is_required:
1✔
968
                if is_array and flex_dim > 0:
1✔
969
                    block = _render_flex_set_block(
×
970
                        name,
971
                        len(type_info.dimensions),
972
                        flex_dim,
973
                        flex_bounds or [],
974
                    )
975
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
×
976
                    set_present_assignment = (
×
977
                        f"if (present({name})) then\n{indented_block}\nend if"
978
                    )
979
                elif is_array and has_default:
1✔
980
                    # Preserve partial-update semantics for arrays with defaults,
981
                    # including runtime-sized arrays driven by constants.
982
                    block = _render_partial_set_block(
1✔
983
                        name,
984
                        len(type_info.dimensions),
985
                        partial_bounds or [],
986
                    )
987
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
1✔
988
                    set_present_assignment = (
1✔
989
                        f"if (present({name})) then\n{indented_block}\nend if"
990
                    )
991
                elif is_array and dynamic_array:
1✔
992
                    block = _render_exact_set_block(
1✔
993
                        name,
994
                        len(type_info.dimensions),
995
                    )
996
                    indented_block = "\n".join(f"  {line}" for line in block.splitlines())
1✔
997
                    set_present_assignment = (
1✔
998
                        f"if (present({name})) then\n{indented_block}\nend if"
999
                    )
1000
                else:
1001
                    set_present_assignment = f"if (present({name})) this%{name} = {name}"
1✔
1002
            else:
1003
                set_present_assignment = None
1✔
1004

1005
            array_rank = len(type_info.dimensions) if is_array else 0
1✔
1006
            element_condition: str | None = None
1✔
1007

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

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

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

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

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

1104
    set_dims_arguments = [
1✔
1105
        {
1106
            "name": entry["name"],
1107
            "default_name": entry["default_name"],
1108
            "local_name": entry["local_name"],
1109
            "arg_name": entry["name"],
1110
            "candidate_name": f"candidate_{entry['name']}",
1111
            "min_required": 1,
1112
        }
1113
        for entry in runtime_dimensions
1114
    ]
1115

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

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

1208
    return context
1✔
1209

1210

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

1220

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

1236

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

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

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

1273

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

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

1312

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

1329

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

1337

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

1344

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

1429

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

1462

1463
_FORTRAN_IDENTIFIER = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$")
1✔
1464

1465

1466
def _normalize_constant_values(
1✔
1467
    constants: dict[str, int] | None,
1468
) -> dict[str, int]:
1469
    if constants is None:
1✔
1470
        return {}
1✔
1471
    normalized: dict[str, int] = {}
1✔
1472
    for name, value in constants.items():
1✔
1473
        if not isinstance(name, str) or not name.strip():
1✔
NEW
1474
            raise ValueError("constant names must be non-empty strings")
×
1475
        if not _FORTRAN_IDENTIFIER.match(name):
1✔
NEW
1476
            raise ValueError(f"constant '{name}' must be a valid Fortran identifier")
×
1477
        canonical_name = name.lower()
1✔
1478
        if canonical_name in normalized:
1✔
NEW
1479
            raise ValueError(f"constant '{name}' duplicates another constant name")
×
1480
        if isinstance(value, bool) or not isinstance(value, int):
1✔
1481
            raise ValueError(f"constant '{name}' must be an integer")
1✔
1482
        normalized[canonical_name] = value
1✔
1483
    return normalized
1✔
1484

1485

1486
def _validate_runtime_dimensions(dimensions: dict[str, int] | None) -> dict[str, int]:
1✔
1487
    if dimensions is None:
1✔
1488
        return {}
1✔
1489
    normalized: dict[str, int] = {}
1✔
1490
    for name, value in dimensions.items():
1✔
1491
        if not isinstance(name, str) or not name.strip():
1✔
NEW
1492
            raise ValueError("runtime dimension names must be non-empty strings")
×
1493
        if not _FORTRAN_IDENTIFIER.match(name):
1✔
1494
            raise ValueError(f"runtime dimension '{name}' must be a valid Fortran identifier")
1✔
1495
        canonical_name = name.lower()
1✔
1496
        if canonical_name in normalized:
1✔
NEW
1497
            raise ValueError(f"runtime dimension '{name}' duplicates another dimension name")
×
1498
        if isinstance(value, bool) or not isinstance(value, int):
1✔
1499
            raise ValueError(f"runtime dimension '{name}' must be an integer")
1✔
1500
        if value <= 0:
1✔
1501
            raise ValueError(f"runtime dimension '{name}' must be positive")
1✔
1502
        normalized[canonical_name] = value
1✔
1503
    return normalized
1✔
1504

1505

1506
def _is_int_literal(value: str) -> bool:
1✔
1507
    try:
1✔
1508
        int(value)
1✔
1509
    except ValueError:
1✔
1510
        return False
1✔
1511
    return True
1✔
1512

1513

1514
def _validate_dimension_token(dim: str) -> None:
1✔
1515
    if dim == ":":
1✔
1516
        return
×
1517
    if _is_int_literal(dim):
1✔
1518
        return
1✔
1519
    if _FORTRAN_IDENTIFIER.match(dim):
1✔
1520
        return
1✔
1521
    raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
1522

1523

1524
def _validate_length_token(length_expr: str) -> None:
1✔
1525
    if _is_int_literal(length_expr):
1✔
1526
        return
×
1527
    if _FORTRAN_IDENTIFIER.match(length_expr):
1✔
1528
        return
1✔
1529
    raise ValueError("string length must be an integer literal or identifier")
×
1530

1531

1532
def _parse_flex_dim(prop: dict[str, Any], type_info: FieldTypeInfo) -> int:
1✔
1533
    flex_raw = prop.get("x-fortran-flex-tail-dims")
1✔
1534
    if flex_raw is None:
1✔
1535
        flex_value = 0
1✔
1536
    else:
1537
        if isinstance(flex_raw, bool) or not isinstance(flex_raw, int):
1✔
1538
            raise ValueError("x-fortran-flex-tail-dims must be an integer")
×
1539
        flex_value = flex_raw
1✔
1540
    if flex_value < 0:
1✔
1541
        raise ValueError("x-fortran-flex-tail-dims must be >= 0")
×
1542
    if flex_value == 0:
1✔
1543
        return 0
1✔
1544
    if type_info.category != "array":
1✔
1545
        raise ValueError("x-fortran-flex-tail-dims is only supported for arrays")
1✔
1546
    if flex_value > len(type_info.dimensions):
1✔
1547
        raise ValueError("x-fortran-flex-tail-dims must not exceed array rank")
1✔
1548
    if any(dim == ":" for dim in type_info.dimensions):
1✔
1549
        raise ValueError(
×
1550
            "x-fortran-flex-tail-dims does not support deferred-size dimensions"
1551
        )
1552
    return flex_value
1✔
1553

1554

1555
def _collect_dimension_constants(
1✔
1556
    dimensions: list[str],
1557
    constants: dict[str, int] | None,
1558
) -> list[str]:
1559
    used: list[str] = []
1✔
1560
    for dim in dimensions:
1✔
1561
        if dim == ":" or _is_int_literal(dim):
1✔
1562
            continue
1✔
1563
        if not _FORTRAN_IDENTIFIER.match(dim):
1✔
1564
            raise ValueError("array property 'x-fortran-shape' entries must be ints or identifiers")
×
1565
        dim_key = dim.lower()
1✔
1566
        if constants is None or dim_key not in constants:
1✔
1567
            raise ValueError(f"dimension constant '{dim}' is not defined in config")
1✔
1568
        value = constants[dim_key]
1✔
1569
        if isinstance(value, bool) or not isinstance(value, int):
1✔
1570
            raise ValueError(f"dimension constant '{dim}' must be an integer")
×
1571
        if dim not in used:
1✔
1572
            used.append(dim)
1✔
1573
    return used
1✔
1574

1575

1576
def _render_declaration(type_spec: str, dimensions: list[str], name: str) -> str:
1✔
1577
    parts = [type_spec]
1✔
1578
    if dimensions:
1✔
1579
        dims = ", ".join(dimensions)
1✔
1580
        parts.append(f"dimension({dims})")
1✔
1581
    return f"{', '.join(parts)} :: {name}"
1✔
1582

1583

1584
def _render_runtime_declaration(
1✔
1585
    type_info: FieldTypeInfo,
1586
    name: str,
1587
) -> str:
1588
    if type_info.category == "string":
1✔
NEW
1589
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
1590
    if type_info.category != "array":
1✔
NEW
1591
        raise ValueError(
×
1592
            "runtime declaration is only supported for arrays, "
1593
            f"got category '{type_info.category}'"
1594
        )
1595

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

1599

1600
def _render_runtime_local_declaration(
1✔
1601
    type_info: FieldTypeInfo,
1602
    name: str,
1603
    *,
1604
    runtime_dimensions: list[str],
1605
) -> str:
1606
    if type_info.category == "string":
1✔
NEW
1607
        raise ValueError("runtime scalar strings are not supported; only runtime arrays")
×
1608
    if type_info.category != "array":
1✔
NEW
1609
        raise ValueError(
×
1610
            "runtime local declaration is only supported for arrays, "
1611
            f"got category '{type_info.category}'"
1612
        )
1613

1614
    type_spec = type_info.type_spec
1✔
1615
    dims = ", ".join(":" for _ in runtime_dimensions)
1✔
1616
    return f"{type_spec}, allocatable, dimension({dims}) :: {name}"
1✔
1617

1618

1619
def _render_runtime_allocations(
1✔
1620
    type_info: FieldTypeInfo,
1621
    name: str,
1622
    *,
1623
    runtime_dimensions: list[str],
1624
    runtime_length_expr: str | None,
1625
    target_prefix: str = "",
1626
) -> list[str]:
1627
    lines: list[str] = []
1✔
1628
    target_ref = f"{target_prefix}{name}"
1✔
1629
    if type_info.category != "array":
1✔
NEW
1630
        raise ValueError("runtime allocation is only supported for arrays")
×
1631
    if any(dim == ":" for dim in runtime_dimensions):
1✔
NEW
1632
        raise ValueError("runtime-sized arrays do not support deferred-size dimensions")
×
1633

1634
    lines.append(f"if (allocated({target_ref})) deallocate({target_ref})")
1✔
1635
    dims_expr = ", ".join(runtime_dimensions)
1✔
1636
    if type_info.element_category == "string" and runtime_length_expr is not None:
1✔
1637
        lines.append(f"allocate(character(len={runtime_length_expr}) :: {target_ref}({dims_expr}))")
1✔
1638
    else:
1639
        lines.append(f"allocate({target_ref}({dims_expr}))")
1✔
1640
    return lines
1✔
1641

1642

1643
def _render_runtime_deallocations(name: str, *, target_prefix: str = "") -> list[str]:
1✔
1644
    target_ref = f"{target_prefix}{name}"
1✔
1645
    return [f"if (allocated({target_ref})) deallocate({target_ref})"]
1✔
1646

1647

1648
def _render_runtime_local_allocations(
1✔
1649
    type_info: FieldTypeInfo,
1650
    name: str,
1651
    *,
1652
    runtime_dimensions: list[str],
1653
    runtime_length_expr: str | None,
1654
) -> list[str]:
1655
    if type_info.category == "string":
1✔
NEW
1656
        return []
×
1657
    return _render_runtime_allocations(
1✔
1658
        type_info,
1659
        name,
1660
        runtime_dimensions=runtime_dimensions,
1661
        runtime_length_expr=runtime_length_expr,
1662
    )
1663

1664

1665
def _array_section_ref(base_ref: str, rank: int) -> str:
1✔
1666
    dims = ", ".join(":" for _ in range(rank))
1✔
1667
    return f"{base_ref}({dims})"
1✔
1668

1669

1670
def _render_argument_declaration(
1✔
1671
    *,
1672
    name: str,
1673
    type_info: FieldTypeInfo,
1674
    is_required: bool,
1675
    dimensions: list[str] | None = None,
1676
    doc: str | None = None,
1677
) -> str:
1678
    intent = "intent(in)"
1✔
1679
    parts = [type_info.arg_type_spec]
1✔
1680
    arg_dimensions = dimensions if dimensions is not None else type_info.dimensions
1✔
1681
    if arg_dimensions:
1✔
1682
        dims = ", ".join(arg_dimensions)
1✔
1683
        parts.append(f"dimension({dims})")
1✔
1684
    if not is_required:
1✔
1685
        parts.append(intent)
1✔
1686
        parts.append("optional")
1✔
1687
        decl = f"{', '.join(parts[:-1])}, {parts[-1]} :: {name}"
1✔
1688
    else:
1689
        parts.append(intent)
1✔
1690
        decl = f"{', '.join(parts)} :: {name}"
1✔
1691
    if doc:
1✔
1692
        decl = f"{decl} !< {doc}"
1✔
1693
    return decl
1✔
1694

1695

1696
def _sentinel_comment(type_info: FieldTypeInfo, *, required: bool) -> str:
1✔
1697
    label = "required" if required else "optional"
1✔
1698
    category = type_info.category
1✔
1699
    if category == "array":
1✔
1700
        element = type_info.element_category or "array"
1✔
1701
        return f" ! sentinel for {label} {element} array"
1✔
1702
    if category == "string":
1✔
1703
        if required:
1✔
1704
            return " ! NULL string as sentinel for required string"
1✔
1705
        return " ! sentinel for optional string"
1✔
1706
    if category == "integer":
1✔
1707
        return f" ! sentinel for {label} integer"
1✔
1708
    if category == "real":
1✔
1709
        return f" ! sentinel for {label} real"
1✔
1710
    return ""
×
1711

1712

1713
def _render_sentinel_assignment(
1✔
1714
    type_info: FieldTypeInfo,
1715
    *,
1716
    target_ref: str,
1717
    value_expr: str,
1718
    comment: str,
1719
) -> str:
1720
    if type_info.category == "string":
1✔
1721
        return _render_string_sentinel_assignment(
1✔
1722
            target_ref=target_ref,
1723
            value_expr=value_expr,
1724
            comment=comment,
1725
        )
1726
    if type_info.category == "array" and type_info.element_category == "string":
1✔
1727
        return _render_string_array_sentinel_assignment(
1✔
1728
            target_ref=target_ref,
1729
            value_expr=value_expr,
1730
            comment=comment,
1731
        )
1732
    return f"{target_ref} = {value_expr}{comment}"
1✔
1733

1734

1735
def _render_string_sentinel_assignment(
1✔
1736
    *,
1737
    target_ref: str,
1738
    value_expr: str,
1739
    comment: str,
1740
) -> str:
1741
    return f"{target_ref} = {value_expr}{comment}"
1✔
1742

1743

1744
def _render_string_array_sentinel_assignment(
1✔
1745
    *,
1746
    target_ref: str,
1747
    value_expr: str,
1748
    comment: str,
1749
) -> str:
1750
    return f"{target_ref} = {value_expr}{comment}"
1✔
1751

1752

1753
def _sentinel_expressions(
1✔
1754
    type_info: FieldTypeInfo,
1755
    *,
1756
    var_ref: str,
1757
    len_ref: str | None = None,
1758
) -> tuple[str, str, bool]:
1759
    category = type_info.category
1✔
1760
    if category == "array":
1✔
1761
        element = type_info.element_category
1✔
1762
        if element == "string":
1✔
1763
            return "achar(0)", f"all({var_ref} == achar(0))", False
1✔
1764
        if element == "integer":
1✔
1765
            return f"-huge({var_ref})", f"all({var_ref} == -huge({var_ref}))", False
1✔
1766
        if element == "real":
1✔
1767
            return (
1✔
1768
                f"ieee_value({var_ref}, ieee_quiet_nan)",
1769
                f"all(ieee_is_nan({var_ref}))",
1770
                True,
1771
            )
1772
        if element == "boolean":
×
1773
            raise ValueError("boolean arrays cannot use sentinels")
×
1774
        raise ValueError(f"unsupported sentinel array element '{element}'")
×
1775
    if category == "string":
1✔
1776
        return "achar(0)", f"{var_ref} == achar(0)", False
1✔
1777
    if category == "integer":
1✔
1778
        return f"-huge({var_ref})", f"{var_ref} == -huge({var_ref})", False
1✔
1779
    if category == "real":
1✔
1780
        return (
1✔
1781
            f"ieee_value({var_ref}, ieee_quiet_nan)",
1782
            f"ieee_is_nan({var_ref})",
1783
            True,
1784
        )
1785
    if category == "boolean":
×
1786
        raise ValueError("boolean values cannot use sentinels")
×
1787
    raise ValueError(f"unsupported sentinel category '{category}'")
×
1788

1789

1790
def _element_missing_expression(
1✔
1791
    category: str,
1792
    *,
1793
    var_ref: str,
1794
    len_ref: str | None = None,
1795
) -> tuple[str, bool]:
1796
    if category == "string":
1✔
NEW
1797
        return f"{var_ref} == achar(0)", False
×
1798
    if category == "integer":
1✔
1799
        return f"{var_ref} == -huge({var_ref})", False
1✔
1800
    if category == "real":
1✔
1801
        return f"ieee_is_nan({var_ref})", True
1✔
1802
    raise ValueError(f"unsupported missing category '{category}'")
×
1803

1804

1805
def _array_missing_conditions(
1✔
1806
    element_category: str,
1807
    *,
1808
    var_ref: str,
1809
    len_ref: str | None = None,
1810
) -> tuple[str, str, bool]:
1811
    missing_expr, uses_ieee = _element_missing_expression(
1✔
1812
        element_category, var_ref=var_ref, len_ref=len_ref
1813
    )
1814
    return f"all({missing_expr})", f"any({missing_expr})", uses_ieee
1✔
1815

1816

1817
def _slice_ref(name: str, rank: int, dim: int, index_var: str) -> str:
1✔
1818
    dims = [":" for _ in range(rank)]
1✔
1819
    dims[dim - 1] = index_var
1✔
1820
    return f"this%{name}({', '.join(dims)})"
1✔
1821

1822

1823
def _flex_bound_vars(dim: int) -> tuple[str, str]:
1✔
1824
    return f"lb_{dim}", f"ub_{dim}"
1✔
1825

1826

1827
def _slice_ref_bounds(
1✔
1828
    name: str,
1829
    rank: int,
1830
    flex_dims: list[int],
1831
    lb_vars: dict[int, str],
1832
    ub_vars: dict[int, str],
1833
) -> str:
1834
    dims: list[str] = []
1✔
1835
    for dim in range(1, rank + 1):
1✔
1836
        if dim in flex_dims:
1✔
1837
            dims.append(f"{lb_vars[dim]}:{ub_vars[dim]}")
1✔
1838
        else:
1839
            dims.append(":")
1✔
1840
    return f"this%{name}({', '.join(dims)})"
1✔
1841

1842

1843
def _render_flex_set_block(
1✔
1844
    name: str,
1845
    rank: int,
1846
    flex_dim_count: int,
1847
    bounds: list[dict[str, Any]],
1848
) -> str:
1849
    flex_dims = list(range(rank - flex_dim_count + 1, rank + 1))
1✔
1850
    lb_vars = {entry["dim"]: entry["lb_var"] for entry in bounds}
1✔
1851
    ub_vars = {entry["dim"]: entry["ub_var"] for entry in bounds}
1✔
1852
    lines: list[str] = []
1✔
1853
    for dim in range(1, rank - flex_dim_count + 1):
1✔
1854
        lines.append(f"if (size({name}, {dim}) /= size(this%{name}, {dim})) then")
1✔
1855
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
1856
        lines.append(
1✔
1857
            f"  if (present(errmsg)) errmsg = \"dimension {dim} mismatch for '{name}'\""
1858
        )
1859
        lines.append("  return")
1✔
1860
        lines.append("end if")
1✔
1861
    for entry in bounds:
1✔
1862
        dim = entry["dim"]
1✔
1863
        lb_var = entry["lb_var"]
1✔
1864
        ub_var = entry["ub_var"]
1✔
1865
        lines.append(f"if (size({name}, {dim}) > size(this%{name}, {dim})) then")
1✔
1866
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
1867
        lines.append(
1✔
1868
            f"  if (present(errmsg)) errmsg = \"dimension {dim} exceeds bounds for '{name}'\""
1869
        )
1870
        lines.append("  return")
1✔
1871
        lines.append("end if")
1✔
1872
        lines.append(f"{lb_var} = lbound(this%{name}, {dim})")
1✔
1873
        lines.append(f"{ub_var} = {lb_var} + size({name}, {dim}) - 1")
1✔
1874
    target_ref = _slice_ref_bounds(name, rank, flex_dims, lb_vars, ub_vars)
1✔
1875
    lines.append(f"{target_ref} = {name}")
1✔
1876
    return "\n".join(lines)
1✔
1877

1878

1879
def _render_exact_set_block(
1✔
1880
    name: str,
1881
    rank: int,
1882
) -> str:
1883
    lines: list[str] = []
1✔
1884
    for dim in range(1, rank + 1):
1✔
1885
        lines.append(f"if (size({name}, {dim}) /= size(this%{name}, {dim})) then")
1✔
1886
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
1887
        lines.append(
1✔
1888
            f"  if (present(errmsg)) errmsg = \"dimension {dim} mismatch for '{name}'\""
1889
        )
1890
        lines.append("  return")
1✔
1891
        lines.append("end if")
1✔
1892
    lines.append(f"this%{name} = {name}")
1✔
1893
    return "\n".join(lines)
1✔
1894

1895

1896
def _render_partial_set_block(
1✔
1897
    name: str,
1898
    rank: int,
1899
    bounds: list[dict[str, Any]],
1900
) -> str:
1901
    lb_vars = {entry["dim"]: entry["lb_var"] for entry in bounds}
1✔
1902
    ub_vars = {entry["dim"]: entry["ub_var"] for entry in bounds}
1✔
1903
    dims_all = [entry["dim"] for entry in bounds]
1✔
1904
    lines: list[str] = []
1✔
1905
    for entry in bounds:
1✔
1906
        dim = entry["dim"]
1✔
1907
        lb_var = entry["lb_var"]
1✔
1908
        ub_var = entry["ub_var"]
1✔
1909
        lines.append(f"if (size({name}, {dim}) > size(this%{name}, {dim})) then")
1✔
1910
        lines.append("  status = NML_ERR_INVALID_INDEX")
1✔
1911
        lines.append(
1✔
1912
            f"  if (present(errmsg)) errmsg = \"dimension {dim} exceeds bounds for '{name}'\""
1913
        )
1914
        lines.append("  return")
1✔
1915
        lines.append("end if")
1✔
1916
        lines.append(f"{lb_var} = lbound(this%{name}, {dim})")
1✔
1917
        lines.append(f"{ub_var} = {lb_var} + size({name}, {dim}) - 1")
1✔
1918
    target_ref = _slice_ref_bounds(name, rank, dims_all, lb_vars, ub_vars)
1✔
1919
    lines.append(f"{target_ref} = {name}")
1✔
1920
    return "\n".join(lines)
1✔
1921

1922

1923
def _sort_bound_vars(values: set[str]) -> list[str]:
1✔
1924
    def sort_key(name: str) -> tuple[str, int]:
1✔
1925
        prefix, _, suffix = name.partition("_")
1✔
1926
        try:
1✔
1927
            return prefix, int(suffix)
1✔
1928
        except ValueError:
×
1929
            return prefix, 0
×
1930

1931
    return sorted(values, key=sort_key)
1✔
1932

1933

1934
def _format_reshape_assignment(name: str, arguments: list[str]) -> str:
1✔
1935
    lines = [f"this%{name} = reshape( &"]
1✔
1936
    for index, arg in enumerate(arguments):
1✔
1937
        suffix = ", &" if index < len(arguments) - 1 else ")"
1✔
1938
        lines.append(f"  {arg}{suffix}")
1✔
1939
    return "\n".join(lines)
1✔
1940

1941

1942
def _format_default(
1✔
1943
    value: Any,
1944
    type_info: FieldTypeInfo,
1945
    prop: dict[str, Any],
1946
    constants: dict[str, int] | None = None,
1947
) -> str:
1948
    if type_info.category == "array":
1✔
1949
        if not isinstance(value, list):
×
1950
            raise ValueError("array default must be a list")
×
1951
        parsed_dims = _parse_default_dimensions(type_info.dimensions, constants)
×
1952
        array_default = _prepare_array_default(value, parsed_dims, prop)
×
1953
        elements = [
×
1954
            _format_scalar_default(element, type_info.kind, type_info.element_category)
1955
            for element in array_default.source_values
1956
        ]
1957
        if (
×
1958
            len(type_info.dimensions) == 1
1959
            and array_default.order_values is None
1960
            and array_default.pad_values is None
1961
        ):
1962
            return f"[{', '.join(elements)}]"
×
1963

1964
        shape_literal = ", ".join(type_info.dimensions)
×
1965
        arguments = [f"[{', '.join(elements)}]", f"shape=[{shape_literal}]"]
×
1966

1967
        if array_default.order_values is not None:
×
1968
            order_literal = ", ".join(str(index) for index in array_default.order_values)
×
1969
            arguments.append(f"order=[{order_literal}]")
×
1970

1971
        if array_default.pad_values is not None:
×
1972
            pad_elements = [
×
1973
                _format_scalar_default(element, type_info.kind, type_info.element_category)
1974
                for element in array_default.pad_values
1975
            ]
1976
            arguments.append(f"pad=[{', '.join(pad_elements)}]")
×
1977

1978
        return f"reshape({', '.join(arguments)})"
×
1979
    return _format_scalar_default(value, type_info.kind, type_info.category)
1✔
1980

1981

1982
def _format_scalar_default(value: Any, kind: str | None, category: str | None) -> str:
1✔
1983
    if category == "integer":
1✔
1984
        if not isinstance(value, int):
1✔
1985
            raise ValueError("integer default must be an int")
×
1986
        suffix = f"_{kind}" if kind else ""
1✔
1987
        return f"{value}{suffix}"
1✔
1988
    if category == "real":
1✔
1989
        number = float(value)
1✔
1990
        literal = repr(number)
1✔
1991
        if literal.lower() == "nan":
1✔
1992
            raise ValueError("NaN defaults are not supported")
×
1993
        if "E" in literal:
1✔
1994
            literal = literal.replace("E", "e")
×
1995
        if "." not in literal and "e" not in literal:
1✔
1996
            literal = f"{literal}.0"
×
1997
        suffix = f"_{kind}" if kind else ""
1✔
1998
        return f"{literal}{suffix}"
1✔
1999
    if category == "boolean":
1✔
2000
        if not isinstance(value, bool):
1✔
2001
            raise ValueError("boolean default must be a bool")
×
2002
        return ".true." if value else ".false."
1✔
2003
    if category == "string":
1✔
2004
        if not isinstance(value, str):
1✔
2005
            raise ValueError("string default must be a str")
×
2006
        escaped = value.replace('"', '""')
1✔
2007
        return f'"{escaped}"'
1✔
2008
    raise ValueError(f"unsupported default category '{category}'")
×
2009

2010

2011
def _parse_default_dimensions(
1✔
2012
    dimensions: list[str],
2013
    constants: dict[str, int] | None,
2014
) -> list[int]:
2015
    if not dimensions:
1✔
2016
        raise ValueError("array property missing dimensions")
×
2017
    parsed: list[int] = []
1✔
2018
    for dim in dimensions:
1✔
2019
        if dim == ":":
1✔
2020
            raise ValueError("defaults not supported for deferred-size dimensions")
×
2021
        try:
1✔
2022
            parsed.append(int(dim))
1✔
2023
        except (TypeError, ValueError) as err:  # pragma: no cover - defensive
2024
            dim_key = dim.lower()
2025
            if constants is None or dim_key not in constants:
2026
                raise ValueError(
2027
                    "array default dimensions must be integer literals or defined constants"
2028
                ) from err
2029
            value = constants[dim_key]
2030
            if isinstance(value, bool) or not isinstance(value, int):
2031
                raise ValueError("array default dimension constants must be integers") from err
2032
            parsed.append(value)
2033
    return parsed
1✔
2034

2035

2036
def _prepare_array_default(
1✔
2037
    value: list[Any],
2038
    dims: list[int],
2039
    prop: dict[str, Any],
2040
) -> ArrayDefaultSpec:
2041
    default_values = _ensure_flat_scalar_list(value, "array default")
1✔
2042
    if not default_values:
1✔
2043
        raise ValueError("array default must contain at least one value")
×
2044

2045
    total_size = math.prod(dims)
1✔
2046
    if len(default_values) > total_size:
1✔
2047
        raise ValueError("array default longer than declared x-fortran-shape")
×
2048

2049
    order_raw = prop.get("x-fortran-default-order", "F")
1✔
2050
    if not isinstance(order_raw, str):
1✔
2051
        raise ValueError("array default order must be 'F' or 'C'")
×
2052
    order = order_raw.upper()
1✔
2053
    if order not in {"F", "C"}:
1✔
2054
        raise ValueError("array default order must be 'F' or 'C'")
×
2055

2056
    repeat_raw = prop.get("x-fortran-default-repeat", False)
1✔
2057
    if not isinstance(repeat_raw, bool):
1✔
2058
        raise ValueError("array default repeat must be a boolean")
×
2059
    repeat = bool(repeat_raw)
1✔
2060

2061
    pad_raw = prop.get("x-fortran-default-pad")
1✔
2062
    pad_values: list[Any] | None = None
1✔
2063
    if pad_raw is not None:
1✔
2064
        if repeat:
1✔
2065
            raise ValueError("array default cannot set both pad and repeat")
×
2066
        if not isinstance(pad_raw, list):
1✔
2067
            pad_raw = [pad_raw]
1✔
2068
        pad_values = _ensure_flat_scalar_list(pad_raw, "array default pad")
1✔
2069
        if not pad_values:
1✔
2070
            raise ValueError("array default pad must contain at least one value")
×
2071

2072
    if len(default_values) < total_size and pad_values is None and not repeat:
1✔
2073
        raise ValueError(
1✔
2074
            "array default shorter than declared x-fortran-shape without pad or repeat"
2075
        )
2076

2077
    if repeat:
1✔
2078
        pad_values = list(default_values)
1✔
2079
        if not pad_values:
1✔
2080
            raise ValueError("array default repeat requires at least one value")
×
2081

2082
    order_values: list[int] | None = None
1✔
2083
    if order == "C" and len(dims) > 1:
1✔
2084
        rank = len(dims)
1✔
2085
        order_values = list(range(rank, 0, -1))
1✔
2086

2087
    return ArrayDefaultSpec(
1✔
2088
        source_values=list(default_values),
2089
        pad_values=pad_values,
2090
        order_values=order_values,
2091
    )
2092

2093

2094
def _ensure_flat_scalar_list(values: list[Any], description: str) -> list[Any]:
1✔
2095
    normalized: list[Any] = []
1✔
2096
    for element in values:
1✔
2097
        if isinstance(element, list):
1✔
2098
            raise ValueError(f"{description} must be a flat list")
×
2099
        normalized.append(element)
1✔
2100
    return normalized
1✔
2101

2102

2103
def _enum_values(
1✔
2104
    prop: dict[str, Any],
2105
    type_info: FieldTypeInfo,
2106
    constants: dict[str, int] | None,
2107
) -> list[Any] | None:
2108
    if type_info.category == "array":
1✔
2109
        enum_raw = _array_items_enum(prop)
1✔
2110
    else:
2111
        enum_raw = prop.get("enum")
1✔
2112
    if enum_raw is None:
1✔
2113
        return None
1✔
2114
    if not isinstance(enum_raw, list) or not enum_raw:
1✔
2115
        raise ValueError("property enum must be a non-empty list")
×
2116
    enum_values = _ensure_flat_scalar_list(enum_raw, "enum")
1✔
2117
    category = _enum_category(type_info)
1✔
2118
    if category not in {"integer", "string"}:
1✔
2119
        raise ValueError("enum only supported for integer or string values")
×
2120
    for value in enum_values:
1✔
2121
        _validate_enum_scalar(value, category, "enum")
1✔
2122
    _validate_enum_defaults(prop, type_info, enum_values, category, constants)
1✔
2123
    _validate_enum_examples(prop, type_info, enum_values, category)
1✔
2124
    return enum_values
1✔
2125

2126

2127
def _bounds_spec(
1✔
2128
    prop: dict[str, Any],
2129
    type_info: FieldTypeInfo,
2130
) -> dict[str, Any] | None:
2131
    if type_info.category == "array":
1✔
2132
        bounds_prop = _array_items_bounds(prop)
1✔
2133
        category = type_info.element_category
1✔
2134
    else:
2135
        bounds_prop = prop
1✔
2136
        category = type_info.category
1✔
2137

2138
    min_value, min_exclusive = _extract_bound_value(
1✔
2139
        bounds_prop, "minimum", "exclusiveMinimum"
2140
    )
2141
    max_value, max_exclusive = _extract_bound_value(
1✔
2142
        bounds_prop, "maximum", "exclusiveMaximum"
2143
    )
2144

2145
    if min_value is None and max_value is None:
1✔
2146
        return None
1✔
2147

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

2151
    if min_value is not None:
1✔
2152
        _validate_bound_scalar(min_value, category, "minimum")
1✔
2153
    if max_value is not None:
1✔
2154
        _validate_bound_scalar(max_value, category, "maximum")
1✔
2155

2156
    if min_value is not None and max_value is not None:
1✔
2157
        min_comp = float(min_value) if category == "real" else int(min_value)
1✔
2158
        max_comp = float(max_value) if category == "real" else int(max_value)
1✔
2159
        if min_exclusive or max_exclusive:
1✔
2160
            if min_comp >= max_comp:
1✔
2161
                raise ValueError("minimum must be less than maximum for exclusive bounds")
×
2162
        else:
2163
            if min_comp > max_comp:
×
2164
                raise ValueError("minimum must be <= maximum")
×
2165

2166
    return {
1✔
2167
        "min_value": min_value,
2168
        "min_exclusive": min_exclusive,
2169
        "max_value": max_value,
2170
        "max_exclusive": max_exclusive,
2171
        "category": category,
2172
    }
2173

2174

2175
def _extract_bound_value(
1✔
2176
    prop: dict[str, Any],
2177
    inclusive_key: str,
2178
    exclusive_key: str,
2179
) -> tuple[Any | None, bool]:
2180
    has_inclusive = inclusive_key in prop
1✔
2181
    has_exclusive = exclusive_key in prop
1✔
2182
    if has_inclusive and has_exclusive:
1✔
2183
        raise ValueError(
×
2184
            f"property must not define both '{inclusive_key}' and '{exclusive_key}'"
2185
        )
2186
    if has_exclusive:
1✔
2187
        value = prop.get(exclusive_key)
1✔
2188
        if value is None:
1✔
2189
            raise ValueError(f"{exclusive_key} must be a number")
×
2190
        return value, True
1✔
2191
    if has_inclusive:
1✔
2192
        value = prop.get(inclusive_key)
1✔
2193
        if value is None:
1✔
2194
            raise ValueError(f"{inclusive_key} must be a number")
×
2195
        return value, False
1✔
2196
    return None, False
1✔
2197

2198

2199
def _validate_bound_scalar(value: Any, category: str, label: str) -> None:
1✔
2200
    if category == "integer":
1✔
2201
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2202
            raise ValueError(f"{label} must be an integer")
×
2203
        return
1✔
2204
    if category == "real":
1✔
2205
        if isinstance(value, bool) or not isinstance(value, (int, float)):
1✔
2206
            raise ValueError(f"{label} must be a number")
×
2207
        if math.isinf(float(value)):
1✔
2208
            raise ValueError(f"{label} must not be infinite")
×
2209
        if math.isnan(float(value)):
1✔
2210
            raise ValueError(f"{label} must not be NaN")
×
2211
        return
1✔
2212
    raise ValueError("bounds only supported for integer or real values")
×
2213

2214

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

2219
    default_defined = "default" in prop
1✔
2220
    items_default = _array_items_default(prop)
1✔
2221
    items_defined = "default" in items_default
1✔
2222
    items_value = items_default.get("default") if items_defined else None
1✔
2223

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

2227
    if items_defined:
1✔
2228
        for key in ("x-fortran-default-order", "x-fortran-default-repeat", "x-fortran-default-pad"):
1✔
2229
            if key in prop:
1✔
2230
                raise ValueError("array items default must not use x-fortran-default-* options")
×
2231
        if isinstance(items_value, list):
1✔
2232
            raise ValueError("array items default must be a scalar")
×
2233
        return items_value, True
1✔
2234

2235
    if default_defined:
1✔
2236
        default_value = prop.get("default")
1✔
2237
        if not isinstance(default_value, list):
1✔
2238
            raise ValueError("array default must be a list")
1✔
2239
        return default_value, False
1✔
2240
    return None
1✔
2241

2242

2243
def _array_items_enum(prop: dict[str, Any]) -> list[Any] | None:
1✔
2244
    current = prop
1✔
2245
    while current.get("type") == "array":
1✔
2246
        if "enum" in current:
1✔
2247
            raise ValueError("array enum must be defined on items")
1✔
2248
        items = current.get("items")
1✔
2249
        if not isinstance(items, dict):
1✔
2250
            raise ValueError("array property must define 'items'")
×
2251
        current = items
1✔
2252
    return current.get("enum")
1✔
2253

2254

2255
def _array_items_default(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2256
    current = prop
1✔
2257
    while current.get("type") == "array":
1✔
2258
        items = current.get("items")
1✔
2259
        if not isinstance(items, dict):
1✔
2260
            raise ValueError("array property must define 'items'")
×
2261
        if items.get("type") == "array":
1✔
2262
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2263
        current = items
1✔
2264
    return current
1✔
2265

2266

2267
def _array_items_bounds(prop: dict[str, Any]) -> dict[str, Any]:
1✔
2268
    current = prop
1✔
2269
    while current.get("type") == "array":
1✔
2270
        for key in ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"):
1✔
2271
            if key in current:
1✔
2272
                raise ValueError("array bounds must be defined on items")
×
2273
        items = current.get("items")
1✔
2274
        if not isinstance(items, dict):
1✔
2275
            raise ValueError("array property must define 'items'")
×
2276
        if items.get("type") == "array":
1✔
2277
            raise ValueError("nested array properties are not supported; use x-fortran-shape")
×
2278
        current = items
1✔
2279
    return current
1✔
2280

2281

2282
def _validate_enum_scalar(value: Any, category: str, label: str) -> None:
1✔
2283
    if category == "integer":
1✔
2284
        if isinstance(value, bool) or not isinstance(value, int):
1✔
2285
            raise ValueError(f"{label} values must be integers")
×
2286
        return
1✔
2287
    if category == "string":
1✔
2288
        if not isinstance(value, str):
1✔
2289
            raise ValueError(f"{label} values must be strings")
×
2290
        return
1✔
2291
    raise ValueError("enum only supported for integer or string values")
×
2292

2293

2294
def _ensure_enum_member(value: Any, enum_values: list[Any], category: str, label: str) -> None:
1✔
2295
    _validate_enum_scalar(value, category, label)
×
2296
    if value not in enum_values:
×
2297
        raise ValueError(f"{label} value must be one of enum values")
×
2298

2299

2300
def _validate_enum_defaults(
1✔
2301
    prop: dict[str, Any],
2302
    type_info: FieldTypeInfo,
2303
    enum_values: list[Any],
2304
    category: str,
2305
    constants: dict[str, int] | None,
2306
) -> None:
2307
    if type_info.category == "array":
1✔
2308
        default_info = _array_default_value(prop)
1✔
2309
        if default_info is not None:
1✔
NEW
2310
            default_value, default_from_items = default_info
×
NEW
2311
            if default_from_items:
×
NEW
2312
                _ensure_enum_member(default_value, enum_values, category, "default")
×
2313
            else:
NEW
2314
                default_values = _ensure_flat_scalar_list(default_value, "array default")
×
NEW
2315
                for value in default_values:
×
NEW
2316
                    _ensure_enum_member(value, enum_values, category, "default")
×
2317
        pad_raw = prop.get("x-fortran-default-pad")
1✔
2318
        if pad_raw is not None:
1✔
2319
            pad_values = pad_raw if isinstance(pad_raw, list) else [pad_raw]
×
2320
            pad_values = _ensure_flat_scalar_list(pad_values, "array default pad")
×
2321
            for value in pad_values:
×
2322
                _ensure_enum_member(value, enum_values, category, "pad")
×
2323
        return
1✔
2324
    if "default" not in prop:
1✔
2325
        return
1✔
2326
    default_value = prop["default"]
×
2327
    if isinstance(default_value, list):
×
2328
        raise ValueError("scalar default must not be a list")
×
2329
    _ensure_enum_member(default_value, enum_values, category, "default")
×
2330

2331

2332
def _validate_enum_examples(
1✔
2333
    prop: dict[str, Any],
2334
    type_info: FieldTypeInfo,
2335
    enum_values: list[Any],
2336
    category: str,
2337
) -> None:
2338
    examples = prop.get("examples")
1✔
2339
    if examples is None:
1✔
2340
        return
1✔
2341
    if not isinstance(examples, list):
×
2342
        raise ValueError("property examples must be a list")
×
2343
    for example in examples:
×
2344
        if type_info.category == "array":
×
2345
            if isinstance(example, list):
×
2346
                values = _ensure_flat_scalar_list(example, "array examples")
×
2347
                for value in values:
×
2348
                    _ensure_enum_member(value, enum_values, category, "example")
×
2349
            else:
2350
                _ensure_enum_member(example, enum_values, category, "example")
×
2351
        else:
2352
            if isinstance(example, list):
×
2353
                raise ValueError("scalar examples must not be lists")
×
2354
            _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