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

MuellerSeb / nml-tools / 26472837831

26 May 2026 08:17PM UTC coverage: 81.064% (+1.8%) from 79.274%
26472837831

Pull #31

github

MuellerSeb
Harden validation for unresolved refs and array defaults

Reject reachable raw $ref nodes at the shared default-validation boundary with guidance to call load_schema() or resolve_schema(), so direct validation and generation APIs report the required normalization step clearly.

Reject optional arrays that supply operational defaults without an object items schema, while retaining the existing diagnostic for default controls supplied without an array default. Add regression tests for both paths.
Pull Request #31: Add Reusable Schema Definitions With `$defs` And `$ref`

406 of 477 new or added lines in 6 files covered. (85.12%)

2 existing lines in 1 file now uncovered.

3048 of 3760 relevant lines covered (81.06%)

0.81 hits per line

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

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

3
from __future__ import annotations
1✔
4

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

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

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

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

29

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

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

40

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

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

53

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

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

75

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

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

84

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

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

94

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

125

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

155

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

178

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

204

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1210
    return context
1✔
1211

1212

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

1222

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

1238

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

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

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

1275

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

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

1314

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

1331

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

1339

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

1346

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

1431

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

1464

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

1472

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

1482

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

1490

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

1513

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

1534

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

1542

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

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

1558

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

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

1577

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

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

1601

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

1606

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

1621

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

1626

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

1652

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

1669

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

1691

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

1700

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

1709

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

1746

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

1761

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

1773

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

1779

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

1783

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

1799

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

1826

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

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

1837

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

1845

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

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

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

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

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

1885

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

1914

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

1939

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

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

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

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

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

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

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

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

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

1997

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

2006

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

2030

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

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

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

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

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

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

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

2078

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

2102

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

2118

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

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

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

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

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

2146

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

2158

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

2170

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

2185

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

2197

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

2203

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

2235

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

© 2026 Coveralls, Inc