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

aas-core-works / aas-core-codegen / 23051946462

13 Mar 2026 12:59PM UTC coverage: 83.657% (+1.2%) from 82.428%
23051946462

push

github

web-flow
Turn on coveralls (#596)

The site coveralls.io was down so we had to turn off the coverage
upload temporarily.

30831 of 36854 relevant lines covered (83.66%)

3.35 hits per line

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

86.48
/aas_core_codegen/python/lib/_generate_verification.py
1
"""Generate the invariant verifiers from the intermediate representation."""
2
import io
4✔
3
import textwrap
4✔
4
from typing import (
4✔
5
    Tuple,
6
    Optional,
7
    List,
8
    Sequence,
9
    Mapping,
10
    Union,
11
)
12

13
from icontract import ensure, require
4✔
14

15
from aas_core_codegen import intermediate, specific_implementations
4✔
16
from aas_core_codegen.common import (
4✔
17
    Error,
18
    Stripped,
19
    assert_never,
20
    Identifier,
21
    indent_but_first_line,
22
    wrap_text_into_lines,
23
    assert_union_without_excluded,
24
)
25
from aas_core_codegen.python.common import (
4✔
26
    INDENT as I,
27
    INDENT2 as II,
28
    INDENT3 as III,
29
    INDENT4 as IIII,
30
)
31
from aas_core_codegen.intermediate import type_inference as intermediate_type_inference
4✔
32
from aas_core_codegen.parse import tree as parse_tree, retree as parse_retree
4✔
33
from aas_core_codegen.python import (
4✔
34
    common as python_common,
35
    naming as python_naming,
36
    description as python_description,
37
    transpilation as python_transpilation,
38
)
39

40

41
# region Verify
42

43

44
def verify(
4✔
45
    spec_impls: specific_implementations.SpecificImplementations,
46
    verification_functions: Sequence[intermediate.Verification],
47
) -> Optional[List[str]]:
48
    """Verify all the implementation snippets related to verification."""
49
    errors = []  # type: List[str]
4✔
50

51
    expected_keys = []  # type: List[specific_implementations.ImplementationKey]
4✔
52

53
    for func in verification_functions:
4✔
54
        if isinstance(func, intermediate.ImplementationSpecificVerification):
4✔
55
            expected_keys.append(
4✔
56
                specific_implementations.ImplementationKey(
57
                    f"Verification/{func.name}.py"
58
                ),
59
            )
60

61
    for key in expected_keys:
4✔
62
        if key not in spec_impls:
4✔
63
            errors.append(f"The implementation snippet is missing for: {key}")
×
64

65
    if len(errors) == 0:
4✔
66
        return None
4✔
67

68
    return errors
×
69

70

71
# endregion
72

73
# region Generate
74

75

76
class _PatternVerificationTranspiler(
4✔
77
    parse_tree.RestrictedTransformer[Tuple[Optional[Stripped], Optional[Error]]]
78
):
79
    """Transpile a statement of a pattern verification into Python."""
80

81
    @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
82
    def _transform_joined_str_values(
4✔
83
        self, values: Sequence[Union[str, parse_tree.FormattedValue]]
84
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
85
        """Transform the values of a joined string to a Python string literal."""
86
        # If we do not need interpolation, simply return the string literals
87
        # joined together by newlines.
88
        needs_interpolation = any(
4✔
89
            isinstance(value, parse_tree.FormattedValue) for value in values
90
        )
91
        if not needs_interpolation:
4✔
92
            return (
4✔
93
                Stripped(
94
                    python_common.string_literal(
95
                        "".join(value for value in values)  # type: ignore
96
                    )
97
                ),
98
                None,
99
            )
100

101
        parts = []  # type: List[str]
4✔
102

103
        # NOTE (mristin, 2022-09-30):
104
        # See which quotes occur more often in the non-interpolated parts, so that we
105
        # pick the escaping scheme which will result in as little escapes as possible.
106
        double_quotes_count = 0
4✔
107
        single_quotes_count = 0
4✔
108

109
        for value in values:
4✔
110
            if isinstance(value, str):
4✔
111
                double_quotes_count += value.count('"')
4✔
112
                single_quotes_count += value.count("'")
4✔
113

114
            elif isinstance(value, parse_tree.FormattedValue):
4✔
115
                pass
4✔
116
            else:
117
                assert_never(value)
×
118

119
        # Pick the escaping scheme
120
        if single_quotes_count <= double_quotes_count:
4✔
121
            enclosing = "'"
4✔
122
            quoting = python_common.StringQuoting.SINGLE_QUOTES
4✔
123
        else:
124
            enclosing = '"'
×
125
            quoting = python_common.StringQuoting.DOUBLE_QUOTES
×
126

127
        for value in values:
4✔
128
            if isinstance(value, str):
4✔
129
                parts.append(
4✔
130
                    python_common.string_literal(
131
                        value,
132
                        quoting=quoting,
133
                        without_enclosing=True,
134
                        duplicate_curly_brackets=True,
135
                    )
136
                )
137

138
            elif isinstance(value, parse_tree.FormattedValue):
4✔
139
                code, error = self.transform(value.value)
4✔
140
                if error is not None:
4✔
141
                    return None, error
×
142

143
                assert code is not None
4✔
144

145
                assert (
4✔
146
                    "\n" not in code
147
                ), f"New-lines are not expected in formatted values, but got: {code}"
148

149
                parts.append(f"{{{code}}}")
4✔
150
            else:
151
                assert_never(value)
×
152

153
        writer = io.StringIO()
4✔
154
        writer.write("f")
4✔
155
        writer.write(enclosing)
4✔
156
        for part in parts:
4✔
157
            writer.write(part)
4✔
158
        writer.write(enclosing)
4✔
159

160
        return Stripped(writer.getvalue()), None
4✔
161

162
    @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
163
    def transform_constant(
4✔
164
        self, node: parse_tree.Constant
165
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
166
        if isinstance(node.value, str):
4✔
167
            # NOTE (mristin, 2022-06-11):
168
            # We assume that all the string constants are valid regular expressions.
169

170
            regex, parse_error = parse_retree.parse(values=[node.value])
4✔
171
            if parse_error is not None:
4✔
172
                regex_line, pointer_line = parse_retree.render_pointer(
×
173
                    parse_error.cursor
174
                )
175

176
                return (
×
177
                    None,
178
                    Error(
179
                        node.original_node,
180
                        f"The string constant could not be parsed "
181
                        f"as a regular expression: \n"
182
                        f"{parse_error.message}\n"
183
                        f"{regex_line}\n"
184
                        f"{pointer_line}",
185
                    ),
186
                )
187

188
            assert regex is not None
4✔
189

190
            # NOTE (mristin, 2022-09-30):
191
            # Strictly speaking, this is a joined string with a single value, a string
192
            # literal.
193
            return self._transform_joined_str_values(
4✔
194
                values=parse_retree.render(regex=regex)
195
            )
196
        else:
197
            raise AssertionError(f"Unexpected {node=}")
×
198

199
    @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
200
    def transform_name(
4✔
201
        self, node: parse_tree.Name
202
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
203
        return Stripped(python_naming.variable_name(node.identifier)), None
4✔
204

205
    @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
206
    def transform_joined_str(
4✔
207
        self, node: parse_tree.JoinedStr
208
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
209
        regex, parse_error = parse_retree.parse(values=node.values)
4✔
210
        if parse_error is not None:
4✔
211
            regex_line, pointer_line = parse_retree.render_pointer(parse_error.cursor)
×
212

213
            return (
×
214
                None,
215
                Error(
216
                    node.original_node,
217
                    f"The joined string could not be parsed "
218
                    f"as a regular expression: \n"
219
                    f"{parse_error.message}\n"
220
                    f"{regex_line}\n"
221
                    f"{pointer_line}",
222
                ),
223
            )
224

225
        assert regex is not None
4✔
226

227
        return self._transform_joined_str_values(
4✔
228
            values=parse_retree.render(regex=regex)
229
        )
230

231
    def transform_assignment(
4✔
232
        self, node: parse_tree.Assignment
233
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
234
        assert isinstance(node.target, parse_tree.Name)
4✔
235
        variable = python_naming.variable_name(node.target.identifier)
4✔
236
        code, error = self.transform(node.value)
4✔
237
        if error is not None:
4✔
238
            return None, error
×
239
        assert code is not None
4✔
240

241
        return Stripped(f"{variable} = {code}"), None
4✔
242

243

244
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
245
def _transpile_pattern_verification(
4✔
246
    verification: intermediate.PatternVerification,
247
    qualified_module_name: python_common.QualifiedModuleName,
248
) -> Tuple[Optional[Stripped], Optional[Error]]:
249
    """Generate the verification function that checks the regular expressions."""
250
    # NOTE (mristin, 2022-09-30):
251
    # We assume that we performed all the checks at the intermediate stage.
252

253
    construct_name = python_naming.function_name(
4✔
254
        Identifier(f"_construct_{verification.name}")
255
    )
256

257
    blocks = []  # type: List[Stripped]
4✔
258

259
    # region Construct block
260

261
    writer = io.StringIO()
4✔
262
    writer.write(
4✔
263
        f"""\
264
# noinspection SpellCheckingInspection
265
def {construct_name}() -> Pattern[str]:
266
"""
267
    )
268

269
    transpiler = _PatternVerificationTranspiler()
4✔
270

271
    for i, stmt in enumerate(verification.parsed.body):
4✔
272
        if i == len(verification.parsed.body) - 1:
4✔
273
            break
4✔
274

275
        code, error = transpiler.transform(stmt)
4✔
276
        if error is not None:
4✔
277
            return None, error
×
278
        assert code is not None
4✔
279

280
        writer.write(textwrap.indent(code, I))
4✔
281
        writer.write("\n")
4✔
282

283
    if len(verification.parsed.body) >= 2:
4✔
284
        writer.write("\n")
4✔
285

286
    pattern_expr, error = transpiler.transform(verification.pattern_expr)
4✔
287
    if error is not None:
4✔
288
        return None, error
×
289
    assert pattern_expr is not None
4✔
290

291
    # A pragmatic heuristics for breaking lines
292
    if len(pattern_expr) < 50:
4✔
293
        writer.write(textwrap.indent(f"return re.compile({pattern_expr})", I))
4✔
294
    else:
295
        writer.write(
×
296
            textwrap.indent(
297
                f"""\
298
return re.compile(
299
{I}{indent_but_first_line(pattern_expr, I)}
300
)""",
301
                I,
302
            )
303
        )
304

305
    blocks.append(Stripped(writer.getvalue()))
4✔
306

307
    # endregion
308

309
    # region Initialize the regex
310

311
    regex_name = python_naming.constant_name(Identifier(f"_regex_{verification.name}"))
4✔
312

313
    blocks.append(Stripped(f"{regex_name} = {construct_name}()"))
4✔
314

315
    assert len(verification.arguments) == 1
4✔
316
    assert isinstance(
4✔
317
        verification.arguments[0].type_annotation, intermediate.PrimitiveTypeAnnotation
318
    )
319
    # noinspection PyUnresolvedReferences
320
    assert (
4✔
321
        verification.arguments[0].type_annotation.a_type
322
        == intermediate.PrimitiveType.STR
323
    )
324

325
    arg_name = python_naming.argument_name(verification.arguments[0].name)
4✔
326

327
    function_name = python_naming.function_name(verification.name)
4✔
328

329
    writer = io.StringIO()
4✔
330
    writer.write(
4✔
331
        f"""\
332
def {function_name}({arg_name}: str) -> bool:
333
"""
334
    )
335

336
    if verification.description is not None:
4✔
337
        (
4✔
338
            docstring,
339
            docstring_errors,
340
        ) = python_description.generate_docstring_for_signature(
341
            description=verification.description,
342
            context=python_description.Context(
343
                qualified_module_name=qualified_module_name,
344
                module=Identifier("verification"),
345
                cls_or_enum=None,
346
            ),
347
        )
348
        if docstring_errors is not None:
4✔
349
            return None, Error(
×
350
                verification.description.parsed.node,
351
                "Failed to generate the docstring",
352
                docstring_errors,
353
            )
354

355
        assert docstring is not None
4✔
356

357
        writer.write(textwrap.indent(docstring, I))
4✔
358
        writer.write("\n")
4✔
359

360
    writer.write(f"{I}return {regex_name}.match({arg_name}) is not None")
4✔
361

362
    blocks.append(Stripped(writer.getvalue()))
4✔
363

364
    # endregion
365

366
    writer = io.StringIO()
4✔
367
    for i, block in enumerate(blocks):
4✔
368
        if i > 0:
4✔
369
            writer.write("\n\n\n")
4✔
370

371
        writer.write(block)
4✔
372

373
    return Stripped(writer.getvalue()), None
4✔
374

375

376
class _TranspilableVerificationTranspiler(python_transpilation.Transpiler):
4✔
377
    """Transpile the body of a :class:`.TranspilableVerification`."""
378

379
    # fmt: off
380
    @require(
4✔
381
        lambda environment, verification:
382
        all(
383
            environment.find(arg.name) is not None
384
            for arg in verification.arguments
385
        ),
386
        "All arguments defined in the environment"
387
    )
388
    # fmt: on
389
    def __init__(
4✔
390
        self,
391
        type_map: Mapping[
392
            parse_tree.Node, intermediate_type_inference.TypeAnnotationUnion
393
        ],
394
        environment: intermediate_type_inference.Environment,
395
        symbol_table: intermediate.SymbolTable,
396
        verification: intermediate.TranspilableVerification,
397
    ) -> None:
398
        """Initialize with the given values."""
399
        python_transpilation.Transpiler.__init__(
4✔
400
            self, type_map=type_map, environment=environment
401
        )
402

403
        self._symbol_table = symbol_table
4✔
404

405
        self._argument_name_set = frozenset(arg.name for arg in verification.arguments)
4✔
406

407
    def transform_name(
4✔
408
        self, node: parse_tree.Name
409
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
410
        if node.identifier in self._variable_name_set:
4✔
411
            return Stripped(python_naming.variable_name(node.identifier)), None
×
412

413
        if node.identifier in self._argument_name_set:
4✔
414
            return Stripped(python_naming.argument_name(node.identifier)), None
4✔
415

416
        if node.identifier in self._symbol_table.constants_by_name:
4✔
417
            constant_name = python_naming.constant_name(node.identifier)
4✔
418
            return Stripped(f"aas_constants.{constant_name}"), None
4✔
419

420
        if node.identifier in self._symbol_table.verification_functions_by_name:
4✔
421
            return Stripped(python_naming.function_name(node.identifier)), None
×
422

423
        our_type = self._symbol_table.find_our_type(name=node.identifier)
4✔
424
        if isinstance(our_type, intermediate.Enumeration):
4✔
425
            return (
4✔
426
                Stripped(f"aas_types.{python_naming.enum_name(node.identifier)}"),
427
                None,
428
            )
429

430
        return None, Error(
×
431
            node.original_node,
432
            f"We can not determine how to transpile the name {node.identifier!r} "
433
            f"to Python. We could not find it neither in the constants, nor in "
434
            f"verification functions, nor as an enumeration. "
435
            f"If you expect this name to be transpilable, please contact "
436
            f"the developers.",
437
        )
438

439

440
def _transpile_transpilable_verification(
4✔
441
    verification: intermediate.TranspilableVerification,
442
    symbol_table: intermediate.SymbolTable,
443
    environment: intermediate_type_inference.Environment,
444
    qualified_module_name: python_common.QualifiedModuleName,
445
) -> Tuple[Optional[Stripped], Optional[Error]]:
446
    """Transpile a verification function."""
447
    # fmt: off
448
    type_inference, error = (
4✔
449
        intermediate_type_inference.infer_for_verification(
450
            verification=verification,
451
            base_environment=environment
452
        )
453
    )
454
    # fmt: on
455

456
    if error is not None:
4✔
457
        return None, error
×
458

459
    assert type_inference is not None
4✔
460

461
    transpiler = _TranspilableVerificationTranspiler(
4✔
462
        type_map=type_inference.type_map,
463
        environment=type_inference.environment_with_args,
464
        symbol_table=symbol_table,
465
        verification=verification,
466
    )
467

468
    body = []  # type: List[Stripped]
4✔
469
    for node in verification.parsed.body:
4✔
470
        stmt, error = transpiler.transform(node)
4✔
471
        if error is not None:
4✔
472
            return None, Error(
×
473
                verification.parsed.node,
474
                f"Failed to transpile the verification function {verification.name!r}",
475
                [error],
476
            )
477

478
        assert stmt is not None
4✔
479
        body.append(stmt)
4✔
480

481
    writer = io.StringIO()
4✔
482

483
    function_name = python_naming.function_name(verification.name)
4✔
484

485
    if verification.returns is None:
4✔
486
        return_type = "None"
×
487
    else:
488
        return_type = python_common.generate_type(
4✔
489
            type_annotation=verification.returns, types_module=Identifier("aas_types")
490
        )
491

492
    arg_defs = []  # type: List[Stripped]
4✔
493
    for arg in verification.arguments:
4✔
494
        arg_type = python_common.generate_type(
4✔
495
            arg.type_annotation, types_module=Identifier("aas_types")
496
        )
497
        arg_name = python_naming.argument_name(arg.name)
4✔
498
        arg_defs.append(Stripped(f"{arg_name}: {arg_type}"))
4✔
499

500
    if len(arg_defs) == 0:
4✔
501
        writer.write(
×
502
            f"""\
503
def {function_name}() -> {return_type}:"""
504
        )
505
    else:
506
        writer.write(
4✔
507
            f"""\
508
def {function_name}(
509
"""
510
        )
511

512
        for i, arg_def in enumerate(arg_defs):
4✔
513
            if i > 0:
4✔
514
                writer.write(",\n")
4✔
515
            writer.write(textwrap.indent(arg_def, I))
4✔
516

517
        writer.write("\n")
4✔
518
        writer.write(
4✔
519
            f"""\
520
) -> {return_type}:"""
521
        )
522

523
    docstring = None  # type: Optional[Stripped]
4✔
524
    if verification.description is not None:
4✔
525
        (
4✔
526
            docstring,
527
            docstring_errors,
528
        ) = python_description.generate_docstring_for_signature(
529
            description=verification.description,
530
            context=python_description.Context(
531
                qualified_module_name=qualified_module_name,
532
                module=Identifier("verification"),
533
                cls_or_enum=None,
534
            ),
535
        )
536
        if docstring_errors is not None:
4✔
537
            return None, Error(
×
538
                verification.description.parsed.node,
539
                "Failed to generate the docstring",
540
                docstring_errors,
541
            )
542

543
        assert docstring is not None
4✔
544

545
        writer.write("\n")
4✔
546
        writer.write(textwrap.indent(docstring, I))
4✔
547

548
    writer.write(f"\n{I}# pylint: disable=all")
4✔
549

550
    if docstring is None and len(body) == 0:
4✔
551
        writer.write(f"\n{I}pass")
×
552

553
    for stmt in body:
4✔
554
        writer.write("\n")
4✔
555
        writer.write(textwrap.indent(stmt, I))
4✔
556

557
    return Stripped(writer.getvalue()), None
4✔
558

559

560
class _InvariantTranspiler(python_transpilation.Transpiler):
4✔
561
    def __init__(
4✔
562
        self,
563
        type_map: Mapping[
564
            parse_tree.Node, intermediate_type_inference.TypeAnnotationUnion
565
        ],
566
        environment: intermediate_type_inference.Environment,
567
        symbol_table: intermediate.SymbolTable,
568
    ) -> None:
569
        """Initialize with the given values."""
570
        python_transpilation.Transpiler.__init__(
4✔
571
            self, type_map=type_map, environment=environment
572
        )
573

574
        self._symbol_table = symbol_table
4✔
575

576
    def transform_name(
4✔
577
        self, node: parse_tree.Name
578
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
579
        if node.identifier in self._variable_name_set:
4✔
580
            return Stripped(python_naming.variable_name(node.identifier)), None
4✔
581

582
        if node.identifier == "self":
4✔
583
            # The ``that`` refers to the argument of the verification function.
584
            return Stripped("that"), None
4✔
585

586
        if node.identifier in self._symbol_table.constants_by_name:
4✔
587
            constant_name = python_naming.constant_name(node.identifier)
4✔
588
            return Stripped(f"aas_constants.{constant_name}"), None
4✔
589

590
        if node.identifier in self._symbol_table.verification_functions_by_name:
4✔
591
            return Stripped(python_naming.function_name(node.identifier)), None
4✔
592

593
        our_type = self._symbol_table.find_our_type(name=node.identifier)
4✔
594
        if isinstance(our_type, intermediate.Enumeration):
4✔
595
            return (
4✔
596
                Stripped(f"aas_types.{python_naming.enum_name(node.identifier)}"),
597
                None,
598
            )
599

600
        return None, Error(
×
601
            node.original_node,
602
            f"We can not determine how to transpile the name {node.identifier!r} "
603
            f"to Python. We could not find it neither in the local variables, "
604
            f"nor in the global constants, nor in verification functions, "
605
            f"nor as an enumeration. If you expect this name to be transpilable, "
606
            f"please contact the developers.",
607
        )
608

609

610
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
611
def _transpile_invariant(
4✔
612
    invariant: intermediate.Invariant,
613
    symbol_table: intermediate.SymbolTable,
614
    environment: intermediate_type_inference.Environment,
615
) -> Tuple[Optional[Stripped], Optional[Error]]:
616
    """Translate the invariant from the meta-model into Python code."""
617
    # fmt: off
618
    type_map, inference_error = (
4✔
619
        intermediate_type_inference.infer_for_invariant(
620
            invariant=invariant,
621
            environment=environment
622
        )
623
    )
624
    # fmt: on
625

626
    if inference_error is not None:
4✔
627
        return None, inference_error
×
628

629
    assert type_map is not None
4✔
630

631
    transpiler = _InvariantTranspiler(
4✔
632
        type_map=type_map,
633
        environment=environment,
634
        symbol_table=symbol_table,
635
    )
636

637
    expr, error = transpiler.transform(invariant.parsed.body)
4✔
638
    if error is not None:
4✔
639
        return None, error
×
640

641
    assert expr is not None
4✔
642

643
    writer = io.StringIO()
4✔
644
    if len(expr) > 50 or "\n" in expr:
4✔
645
        writer.write("if not (\n")
4✔
646
        writer.write(textwrap.indent(expr, I))
4✔
647
        writer.write("\n):\n")
4✔
648
    else:
649
        no_parenthesis_type_in_this_context = (
4✔
650
            parse_tree.Index,
651
            parse_tree.Name,
652
            parse_tree.Member,
653
            parse_tree.MethodCall,
654
            parse_tree.FunctionCall,
655
        )
656

657
        if isinstance(invariant.parsed.body, no_parenthesis_type_in_this_context):
4✔
658
            not_expr = f"not {expr}"
4✔
659
        else:
660
            not_expr = f"not ({expr})"
4✔
661

662
        writer.write(f"if {not_expr}:\n")
4✔
663

664
    writer.write(f"{I}yield Error(\n")
4✔
665

666
    # NOTE (mristin, 2022-09-30):
667
    # We need to wrap the description in multiple literals as a single long
668
    # string literal is often too much for the readability.
669
    invariant_description_lines = wrap_text_into_lines(invariant.description)
4✔
670

671
    for i, literal in enumerate(invariant_description_lines):
4✔
672
        if i < len(invariant_description_lines) - 1:
4✔
673
            writer.write(f"{II}{python_common.string_literal(literal)} +\n")
4✔
674
        else:
675
            writer.write(f"{II}{python_common.string_literal(literal)}\n")
4✔
676
            writer.write(f"{I})")
4✔
677

678
    return Stripped(writer.getvalue()), None
4✔
679

680

681
OurTypeExceptEnumeration = Union[
4✔
682
    intermediate.ConstrainedPrimitive,
683
    intermediate.AbstractClass,
684
    intermediate.ConcreteClass,
685
]
686
assert_union_without_excluded(
4✔
687
    original_union=intermediate.OurType,
688
    subset_union=OurTypeExceptEnumeration,
689
    excluded=[intermediate.Enumeration],
690
)
691

692

693
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
694
def _generate_verify_property_snippet(
4✔
695
    prop: intermediate.Property,
696
    generator_for_loop_variables: python_common.GeneratorForLoopVariables,
697
) -> Tuple[Optional[Stripped], Optional[Error]]:
698
    """
699
    Generate the snippet to transform a property to verification errors.
700

701
    Return an empty string if there is nothing to be verified for the given property.
702
    """
703
    # NOTE (mristin, 2022-10-01):
704
    # Instead of writing here a complex but general solution with unrolling we choose
705
    # to provide a simple, but limited, solution. First, the meta-model is quite
706
    # limited itself at the moment, so the complexity of the general solution is not
707
    # warranted. Second, we hope that there will be fewer bugs in the simple solution
708
    # which is particularly important at this early adoption stage.
709
    #
710
    # We anticipate that in the future we will indeed need a general and complex
711
    # solution. Here are just some thoughts on how to approach it:
712
    # * Leave the pattern matching to produce more readable code for simple cases,
713
    # * Unroll only in case of composite types and optional composite types.
714

715
    type_anno = (
4✔
716
        prop.type_annotation
717
        if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation)
718
        else prop.type_annotation.value
719
    )
720

721
    if isinstance(type_anno, intermediate.OptionalTypeAnnotation):
4✔
722
        return None, Error(
×
723
            prop.parsed.node,
724
            "We currently implemented verification based on a very limited "
725
            "pattern matching due to code simplicity. We did not handle "
726
            "the case of nested optional values. Please contact "
727
            "the developers if you need this functionality.",
728
        )
729
    elif isinstance(type_anno, intermediate.ListTypeAnnotation):
4✔
730
        if isinstance(type_anno.items, intermediate.OptionalTypeAnnotation):
4✔
731
            return None, Error(
×
732
                prop.parsed.node,
733
                "We currently implemented verification based on a very limited "
734
                "pattern matching due to code simplicity. We did not handle "
735
                "the case of lists of optional values. Please contact "
736
                "the developers if you need this functionality.",
737
            )
738
        elif isinstance(type_anno.items, intermediate.ListTypeAnnotation):
4✔
739
            return None, Error(
×
740
                prop.parsed.node,
741
                "We currently implemented verification based on a very limited "
742
                "pattern matching due to code simplicity. We did not handle "
743
                "the case of lists of lists. Please contact "
744
                "the developers if you need this functionality.",
745
            )
746
        else:
747
            pass
3✔
748
    else:
749
        pass
3✔
750

751
    stmts = []  # type: List[Stripped]
4✔
752

753
    prop_name = python_naming.property_name(prop.name)
4✔
754
    prop_name_literal = python_common.string_literal(prop_name)
4✔
755

756
    if isinstance(type_anno, intermediate.PrimitiveTypeAnnotation):
4✔
757
        # There is nothing that we check for primitive types explicitly. The values
758
        # of the primitive properties are checked at the level of class invariants.
759
        return Stripped(""), None
4✔
760
    elif isinstance(type_anno, intermediate.OurTypeAnnotation):
4✔
761
        if isinstance(type_anno.our_type, intermediate.Enumeration):
4✔
762
            # We rely on mypy to check for valid enumerations, so we do not check
763
            # the enumerations on our side.
764
            return Stripped(""), None
4✔
765

766
        elif isinstance(type_anno.our_type, intermediate.ConstrainedPrimitive):
4✔
767
            function_name = python_naming.function_name(
4✔
768
                Identifier(f"verify_{type_anno.our_type.name}")
769
            )
770

771
            for_error_in_verify = f"for error in {function_name}(that.{prop_name})"
4✔
772
            # Heuristic to break the lines, very rudimentary
773
            if len(for_error_in_verify) > 70:
4✔
774
                for_error_in_verify = f"""\
4✔
775
for error in {function_name}(
776
{II}that.{prop_name}
777
)"""
778

779
            stmts.append(
4✔
780
                Stripped(
781
                    f"""\
782
{for_error_in_verify}:
783
{I}error.path._prepend(
784
{II}PropertySegment(
785
{III}that,
786
{III}{prop_name_literal}
787
{II})
788
{I})
789
{I}yield error"""
790
                )
791
            )
792

793
        elif isinstance(
4✔
794
            type_anno.our_type, (intermediate.AbstractClass, intermediate.ConcreteClass)
795
        ):
796
            for_error_in_self_transform = (
4✔
797
                f"for error in self.transform(that.{prop_name})"
798
            )
799
            # Heuristic to break the lines, very rudimentary
800
            if len(for_error_in_self_transform) > 70:
4✔
801
                for_error_in_self_transform = f"""\
×
802
for error in self.transform(
803
{II}that.{prop_name}
804
)"""
805

806
            stmts.append(
4✔
807
                Stripped(
808
                    f"""\
809
{for_error_in_self_transform}:
810
{I}error.path._prepend(
811
{II}PropertySegment(
812
{III}that,
813
{III}{prop_name_literal}
814
{II})
815
{I})
816
{I}yield error"""
817
                )
818
            )
819
        else:
820
            assert_never(type_anno.our_type)
×
821

822
    elif isinstance(type_anno, intermediate.ListTypeAnnotation):
4✔
823
        assert not isinstance(
4✔
824
            type_anno.items,
825
            (intermediate.OptionalTypeAnnotation, intermediate.ListTypeAnnotation),
826
        ), (
827
            "We chose to implement only a very limited pattern matching; "
828
            "see the note above in the code."
829
        )
830

831
        # NOTE (mristin, 2022-10-01):
832
        # We only descend into our classes here.
833
        if not isinstance(type_anno.items, intermediate.OurTypeAnnotation):
4✔
834
            return Stripped(""), None
×
835

836
        loop_variable = next(generator_for_loop_variables)
4✔
837

838
        for_i_item_in_that_prop = (
4✔
839
            f"for i, {loop_variable} in enumerate(that.{prop_name})"
840
        )
841

842
        # Rudimentary heuristics for line breaking
843
        if len(for_i_item_in_that_prop) > 70:
4✔
844
            for_i_item_in_that_prop = f"""\
4✔
845
for i, {loop_variable} in enumerate(
846
{II}that.{prop_name}
847
)"""
848

849
        for_error_in_self_transform = f"for error in self.transform({loop_variable})"
4✔
850
        if len(for_error_in_self_transform) > 70:
4✔
851
            for_error_in_self_transform = f"""\
×
852
for error in self.transform(
853
{II}{loop_variable}
854
)"""
855

856
        stmts.append(
4✔
857
            Stripped(
858
                f"""\
859
{for_i_item_in_that_prop}:
860
{I}{indent_but_first_line(for_error_in_self_transform, I)}:
861
{II}error.path._prepend(
862
{III}IndexSegment(
863
{IIII}that.{prop_name},
864
{IIII}i
865
{III})
866
{II})
867
{II}error.path._prepend(
868
{III}PropertySegment(
869
{IIII}that,
870
{IIII}{prop_name_literal}
871
{III})
872
{II})
873
{II}yield error"""
874
            )
875
        )
876

877
    else:
878
        assert_never(type_anno)
×
879

880
    verify_block = Stripped("\n".join(stmts))
4✔
881

882
    if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation):
4✔
883
        return (
4✔
884
            Stripped(
885
                f"""\
886
if that.{prop_name} is not None:
887
{I}{indent_but_first_line(verify_block, I)}"""
888
            ),
889
            None,
890
        )
891

892
    return verify_block, None
4✔
893

894

895
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
896
def _generate_transform_for_class(
4✔
897
    cls: intermediate.ConcreteClass,
898
    symbol_table: intermediate.SymbolTable,
899
    base_environment: intermediate_type_inference.Environment,
900
) -> Tuple[Optional[Stripped], Optional[List[Error]]]:
901
    """Generate the transform method to errors for the given concrete class."""
902
    errors = []  # type: List[Error]
4✔
903
    blocks = []  # type: List[Stripped]
4✔
904

905
    environment = intermediate_type_inference.MutableEnvironment(
4✔
906
        parent=base_environment
907
    )
908

909
    assert environment.find(Identifier("self")) is None
4✔
910
    environment.set(
4✔
911
        identifier=Identifier("self"),
912
        type_annotation=intermediate_type_inference.OurTypeAnnotation(our_type=cls),
913
    )
914

915
    for invariant in cls.invariants:
4✔
916
        invariant_code, error = _transpile_invariant(
4✔
917
            invariant=invariant, symbol_table=symbol_table, environment=environment
918
        )
919
        if error is not None:
4✔
920
            errors.append(
×
921
                Error(
922
                    cls.parsed.node,
923
                    f"Failed to transpile the invariant of the class {cls.name!r}",
924
                    [error],
925
                )
926
            )
927
            continue
×
928

929
        assert invariant_code is not None
4✔
930

931
        blocks.append(invariant_code)
4✔
932

933
    if len(errors) > 0:
4✔
934
        return None, errors
×
935

936
    # NOTE (mristin, 2022-10-14):
937
    # We need to generate unique loop variable for each loop since Python tracks
938
    # the variables in function scope, not block scope.
939
    generator_for_loop_variables = python_common.GeneratorForLoopVariables()
4✔
940

941
    for prop in cls.properties:
4✔
942
        block, error = _generate_verify_property_snippet(
4✔
943
            prop=prop, generator_for_loop_variables=generator_for_loop_variables
944
        )
945
        if error is not None:
4✔
946
            errors.append(error)
×
947
        else:
948
            assert block is not None
4✔
949
            if block != "":
4✔
950
                blocks.append(block)
4✔
951

952
    if len(errors) > 0:
4✔
953
        return None, errors
×
954

955
    cls_name = python_naming.class_name(cls.name)
4✔
956

957
    if len(blocks) == 0:
4✔
958
        blocks.append(
4✔
959
            Stripped(
960
                f"""\
961
# No verification has been defined for {cls_name}.
962
return
963
# For this uncommon return-yield construction, see:
964
# https://stackoverflow.com/questions/13243766/how-to-define-an-empty-generator-function
965
# noinspection PyUnreachableCode
966
yield"""
967
            )
968
        )
969

970
    transform_name = python_naming.method_name(Identifier(f"transform_{cls.name}"))
4✔
971

972
    writer = io.StringIO()
4✔
973
    writer.write(
4✔
974
        f"""\
975
# noinspection PyMethodMayBeStatic
976
def {transform_name}(
977
{II}self,
978
{II}that: aas_types.{cls_name}
979
) -> Iterator[Error]:
980
"""
981
    )
982

983
    for i, stmt in enumerate(blocks):
4✔
984
        if i > 0:
4✔
985
            writer.write("\n\n")
4✔
986
        writer.write(textwrap.indent(stmt, I))
4✔
987

988
    return Stripped(writer.getvalue()), None
4✔
989

990

991
def _generate_transformer(
4✔
992
    symbol_table: intermediate.SymbolTable,
993
    base_environment: intermediate_type_inference.Environment,
994
    spec_impls: specific_implementations.SpecificImplementations,
995
) -> Tuple[Optional[Stripped], Optional[List[Error]]]:
996
    """Generate a transformer to double-dispatch an instance to errors."""
997
    errors = []  # type: List[Error]
4✔
998

999
    blocks = []  # type: List[Stripped]
4✔
1000

1001
    # The abstract classes are directly dispatched by the transformer,
1002
    # so we do not need to handle them separately.
1003

1004
    for cls in symbol_table.concrete_classes:
4✔
1005
        if cls.is_implementation_specific:
4✔
1006
            transform_key = specific_implementations.ImplementationKey(
×
1007
                f"Verification/transform_{cls.name}.py"
1008
            )
1009

1010
            implementation = spec_impls.get(transform_key, None)
×
1011
            if implementation is None:
×
1012
                errors.append(
×
1013
                    Error(
1014
                        cls.parsed.node,
1015
                        f"The transformation snippet is missing "
1016
                        f"for the implementation-specific "
1017
                        f"class {cls.name}: {transform_key}",
1018
                    )
1019
                )
1020
                continue
×
1021

1022
            blocks.append(spec_impls[transform_key])
×
1023
        else:
1024
            block, cls_errors = _generate_transform_for_class(
4✔
1025
                cls=cls,
1026
                symbol_table=symbol_table,
1027
                base_environment=base_environment,
1028
            )
1029
            if cls_errors is not None:
4✔
1030
                errors.extend(cls_errors)
×
1031
            else:
1032
                assert block is not None
4✔
1033
                blocks.append(block)
4✔
1034

1035
    if len(errors) > 0:
4✔
1036
        return None, errors
×
1037

1038
    writer = io.StringIO()
4✔
1039
    writer.write(
4✔
1040
        f"""\
1041
class _Transformer(
1042
{II}aas_types.AbstractTransformer[
1043
{III}Iterator[Error]
1044
{II}]
1045
):
1046
"""
1047
    )
1048

1049
    for i, block in enumerate(blocks):
4✔
1050
        if i > 0:
4✔
1051
            writer.write("\n\n")
4✔
1052
        writer.write(textwrap.indent(block, I))
4✔
1053

1054
    return Stripped(writer.getvalue()), None
4✔
1055

1056

1057
def _generate_verify_constrained_primitive(
4✔
1058
    constrained_primitive: intermediate.ConstrainedPrimitive,
1059
    symbol_table: intermediate.SymbolTable,
1060
    base_environment: intermediate_type_inference.Environment,
1061
) -> Tuple[Optional[Stripped], Optional[List[Error]]]:
1062
    """Generate the verify function for the constrained primitives."""
1063
    errors = []  # type: List[Error]
4✔
1064
    blocks = []  # type: List[Stripped]
4✔
1065

1066
    environment = intermediate_type_inference.MutableEnvironment(
4✔
1067
        parent=base_environment
1068
    )
1069

1070
    assert environment.find(Identifier("self")) is None
4✔
1071
    environment.set(
4✔
1072
        identifier=Identifier("self"),
1073
        type_annotation=intermediate_type_inference.OurTypeAnnotation(
1074
            our_type=constrained_primitive
1075
        ),
1076
    )
1077

1078
    for invariant in constrained_primitive.invariants:
4✔
1079
        invariant_code, error = _transpile_invariant(
4✔
1080
            invariant=invariant, symbol_table=symbol_table, environment=environment
1081
        )
1082
        if error is not None:
4✔
1083
            errors.append(
×
1084
                Error(
1085
                    constrained_primitive.parsed.node,
1086
                    f"Failed to transpile the invariant of "
1087
                    f"the constrained primitive {constrained_primitive.name!r}",
1088
                    [error],
1089
                )
1090
            )
1091
            continue
×
1092

1093
        assert invariant_code is not None
4✔
1094

1095
        blocks.append(invariant_code)
4✔
1096

1097
    if len(errors) > 0:
4✔
1098
        return None, errors
×
1099

1100
    no_verification_specified = False
4✔
1101
    if len(blocks) == 0:
4✔
1102
        no_verification_specified = True
4✔
1103
        blocks.append(
4✔
1104
            Stripped(
1105
                """\
1106
# There is no verification specified.
1107
return
1108

1109
# Empty generator according to:
1110
# https://stackoverflow.com/a/13243870/1600678
1111
# noinspection PyUnreachableCode
1112
yield"""
1113
            )
1114
        )
1115

1116
    function_name = python_naming.function_name(
4✔
1117
        Identifier(f"verify_{constrained_primitive.name}")
1118
    )
1119

1120
    that_type = python_common.PRIMITIVE_TYPE_MAP[constrained_primitive.constrainee]
4✔
1121

1122
    writer = io.StringIO()
4✔
1123

1124
    if no_verification_specified:
4✔
1125
        # NOTE (mristin, 2022-10-02):
1126
        # We provide a function for evolvability even though it does nothing.
1127
        writer.write("# noinspection PyUnusedLocal\n")
4✔
1128

1129
    writer.write(
4✔
1130
        f"""\
1131
def {function_name}(
1132
{II}that: {that_type}
1133
) -> Iterator[Error]:
1134
{I}\"\"\"Verify the constraints of :paramref:`that`.\"\"\"
1135
"""
1136
    )
1137

1138
    for i, block in enumerate(blocks):
4✔
1139
        if i > 0:
4✔
1140
            writer.write("\n\n")
4✔
1141
        writer.write(textwrap.indent(block, I))
4✔
1142

1143
    assert len(errors) == 0
4✔
1144
    return Stripped(writer.getvalue()), None
4✔
1145

1146

1147
def _generate_module_docstring(
4✔
1148
    symbol_table: intermediate.SymbolTable,
1149
    qualified_module_name: python_common.QualifiedModuleName,
1150
) -> Stripped:
1151
    """Generate the docstring for the module."""
1152
    docstring_blocks = [
4✔
1153
        Stripped("Verify that the instances of the meta-model satisfy the invariants.")
1154
    ]  # type: List[Stripped]
1155

1156
    first_cls = (
4✔
1157
        symbol_table.concrete_classes[0]
1158
        if len(symbol_table.concrete_classes) > 0
1159
        else None
1160
    )  # type: Optional[intermediate.ConcreteClass]
1161

1162
    if first_cls is not None:
4✔
1163
        cls_name = python_naming.class_name(first_cls.name)
4✔
1164
        an_instance_variable = python_naming.variable_name(Identifier("an_instance"))
4✔
1165

1166
        docstring_blocks.append(
4✔
1167
            Stripped(
1168
                f"""\
1169
Here is an example how to verify an instance of :py:class:`{qualified_module_name}.types.{cls_name}`:
1170

1171
.. code-block::
1172

1173
    import {qualified_module_name}.types as aas_types
1174
    import {qualified_module_name}.verification as aas_verification
1175

1176
    {an_instance_variable} = aas_types.{cls_name}(
1177
        # ... some constructor arguments ...
1178
    )
1179

1180
    for error in aas_verification.verify({an_instance_variable}):
1181
        print(f"{{error.cause}} at: {{error.path}}")"""
1182
            )
1183
        )
1184

1185
    # endregion
1186

1187
    if len(docstring_blocks) == 1:
4✔
1188
        doc_escaped = docstring_blocks[0].replace('"""', '\\"\\"\\"')
×
1189
        docstring = f'"""{doc_escaped}"""'
×
1190
    else:
1191
        doc_escaped = ("\n\n".join(docstring_blocks)).replace('"""', '\\"\\"\\"')
4✔
1192
        docstring = f'''\
4✔
1193
"""
1194
{doc_escaped}
1195
"""'''
1196

1197
    return Stripped(docstring)
4✔
1198

1199

1200
# fmt: off
1201
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
4✔
1202
@ensure(
4✔
1203
    lambda result:
1204
    not (result[0] is not None) or result[0].endswith('\n'),
1205
    "Trailing newline mandatory for valid end-of-files"
1206
)
1207
# fmt: on
1208
def generate(
4✔
1209
    symbol_table: intermediate.SymbolTable,
1210
    qualified_module_name: python_common.QualifiedModuleName,
1211
    spec_impls: specific_implementations.SpecificImplementations,
1212
) -> Tuple[Optional[str], Optional[List[Error]]]:
1213
    """
1214
    Generate the Python code for verification based on the symbol table.
1215

1216
    The ``qualified_module_name`` indicates the fully-qualified name of the base module.
1217
    """
1218
    # region Module docstring
1219
    blocks = [
4✔
1220
        _generate_module_docstring(
1221
            symbol_table=symbol_table, qualified_module_name=qualified_module_name
1222
        ),
1223
        python_common.WARNING,
1224
        Stripped(
1225
            f"""\
1226
import math
1227
import re
1228
import struct
1229
import sys
1230
from typing import (
1231
{I}Callable,
1232
{I}Iterable,
1233
{I}Iterator,
1234
{I}List,
1235
{I}Mapping,
1236
{I}Optional,
1237
{I}Pattern,
1238
{I}Sequence,
1239
{I}Set,
1240
{I}Union
1241
)
1242

1243
if sys.version_info >= (3, 8):
1244
{I}from typing import Final
1245
else:
1246
{I}from typing_extensions import Final
1247

1248
from {qualified_module_name} import (
1249
{I}constants as aas_constants,
1250
{I}types as aas_types,
1251
)"""
1252
        ),
1253
        Stripped(
1254
            f"""\
1255
class PropertySegment:
1256
{I}\"\"\"Represent a property access on a path to an erroneous value.\"\"\"
1257

1258
{I}#: Instance containing the property
1259
{I}instance: Final[aas_types.Class]
1260

1261
{I}#: Name of the property
1262
{I}name: Final[str]
1263

1264
{I}def __init__(
1265
{III}self,
1266
{III}instance: aas_types.Class,
1267
{III}name: str
1268
{I}) -> None:
1269
{II}\"\"\"Initialize with the given values.\"\"\"
1270
{II}self.instance = instance
1271
{II}self.name = name
1272

1273
{I}def __str__(self) -> str:
1274
{II}return f'.{{self.name}}'"""
1275
        ),
1276
        Stripped(
1277
            f"""\
1278
class IndexSegment:
1279
{I}\"\"\"Represent an index access on a path to an erroneous value.\"\"\"
1280

1281
{I}#: Sequence containing the item at :py:attr:`~index`
1282
{I}sequence: Final[Sequence[aas_types.Class]]
1283

1284
{I}#: Index of the item
1285
{I}index: Final[int]
1286

1287
{I}def __init__(
1288
{III}self,
1289
{III}sequence: Sequence[aas_types.Class],
1290
{III}index: int
1291
{I}) -> None:
1292
{II}\"\"\"Initialize with the given values.\"\"\"
1293
{II}self.sequence = sequence
1294
{II}self.index = index
1295

1296
{I}def __str__(self) -> str:
1297
{II}return f'[{{self.index}}]'"""
1298
        ),
1299
        Stripped("Segment = Union[PropertySegment, IndexSegment]"),
1300
        Stripped(
1301
            f"""\
1302
class Path:
1303
{I}\"\"\"Represent the relative path to the erroneous value.\"\"\"
1304

1305
{I}def __init__(self) -> None:
1306
{II}\"\"\"Initialize as an empty path.\"\"\"
1307
{II}self._segments = []  # type: List[Segment]
1308

1309
{I}@property
1310
{I}def segments(self) -> Sequence[Segment]:
1311
{II}\"\"\"Get the segments of the path.\"\"\"
1312
{II}return self._segments
1313

1314
{I}def _prepend(self, segment: Segment) -> None:
1315
{II}\"\"\"Insert the :paramref:`segment` in front of other segments.\"\"\"
1316
{II}self._segments.insert(0, segment)
1317

1318
{I}def __str__(self) -> str:
1319
{II}return "".join(str(segment) for segment in self._segments)"""
1320
        ),
1321
        Stripped(
1322
            f"""\
1323
class Error:
1324
{I}\"\"\"Represent a verification error in the data.\"\"\"
1325

1326
{I}#: Human-readable description of the error
1327
{I}cause: Final[str]
1328

1329
{I}#: Path to the erroneous value
1330
{I}path: Final[Path]
1331

1332
{I}def __init__(self, cause: str) -> None:
1333
{II}\"\"\"Initialize as an error with an empty path.\"\"\"
1334
{II}self.cause = cause
1335
{II}self.path = Path()
1336

1337
{I}def __repr__(self) -> str:
1338
{II}return f"Error(path={{self.path}}, cause={{self.cause}})\""""
1339
        ),
1340
    ]  # type: List[Stripped]
1341

1342
    errors = []  # type: List[Error]
4✔
1343

1344
    base_environment = intermediate_type_inference.populate_base_environment(
4✔
1345
        symbol_table=symbol_table
1346
    )
1347

1348
    for verification in symbol_table.verification_functions:
4✔
1349
        if isinstance(verification, intermediate.ImplementationSpecificVerification):
4✔
1350
            implementation_key = specific_implementations.ImplementationKey(
4✔
1351
                f"Verification/{verification.name}.py"
1352
            )
1353

1354
            implementation = spec_impls.get(implementation_key, None)
4✔
1355
            if implementation is None:
4✔
1356
                errors.append(
×
1357
                    Error(
1358
                        None,
1359
                        f"The snippet for the verification function "
1360
                        f"{verification.name!r} is missing: {implementation_key}",
1361
                    )
1362
                )
1363
            else:
1364
                blocks.append(implementation)
4✔
1365

1366
        elif isinstance(verification, intermediate.PatternVerification):
4✔
1367
            implementation, error = _transpile_pattern_verification(
4✔
1368
                verification=verification, qualified_module_name=qualified_module_name
1369
            )
1370

1371
            if error is not None:
4✔
1372
                errors.append(error)
×
1373
            else:
1374
                assert implementation is not None
4✔
1375
                blocks.append(implementation)
4✔
1376

1377
        elif isinstance(verification, intermediate.TranspilableVerification):
4✔
1378
            implementation, error = _transpile_transpilable_verification(
4✔
1379
                verification=verification,
1380
                symbol_table=symbol_table,
1381
                environment=base_environment,
1382
                qualified_module_name=qualified_module_name,
1383
            )
1384

1385
            if error is not None:
4✔
1386
                errors.append(error)
×
1387
            else:
1388
                assert implementation is not None
4✔
1389
                blocks.append(implementation)
4✔
1390

1391
        else:
1392
            assert_never(verification)
×
1393

1394
    transformer_block, transformer_errors = _generate_transformer(
4✔
1395
        symbol_table=symbol_table,
1396
        base_environment=base_environment,
1397
        spec_impls=spec_impls,
1398
    )
1399
    if transformer_errors is not None:
4✔
1400
        errors.extend(transformer_errors)
×
1401
    else:
1402
        assert transformer_block is not None
4✔
1403
        blocks.append(transformer_block)
4✔
1404

1405
    blocks.append(Stripped("_TRANSFORMER = _Transformer()"))
4✔
1406

1407
    blocks.append(
4✔
1408
        Stripped(
1409
            f"""\
1410
def verify(
1411
{II}that: aas_types.Class
1412
) -> Iterator[Error]:
1413
{I}\"\"\"
1414
{I}Verify the constraints of :paramref:`that` recursively.
1415

1416
{I}:param that: instance whose constraints we want to verify
1417
{I}:yield: constraint violations
1418
{I}\"\"\"
1419
{I}yield from _TRANSFORMER.transform(that)"""
1420
        )
1421
    )
1422

1423
    for our_type in symbol_table.our_types:
4✔
1424
        if isinstance(our_type, intermediate.Enumeration):
4✔
1425
            # NOTE (mristin, 2022-10-01):
1426
            # We do not verify the enumerations explicitly in Python as mypy
1427
            # is capable enough to spot invalid enum literals.
1428
            pass
4✔
1429

1430
        elif isinstance(our_type, intermediate.ConstrainedPrimitive):
4✔
1431
            (
4✔
1432
                constrained_primitive_block,
1433
                constrained_primitive_errors,
1434
            ) = _generate_verify_constrained_primitive(
1435
                constrained_primitive=our_type,
1436
                symbol_table=symbol_table,
1437
                base_environment=base_environment,
1438
            )
1439

1440
            if constrained_primitive_errors is not None:
4✔
1441
                errors.extend(constrained_primitive_errors)
×
1442
            else:
1443
                assert constrained_primitive_block is not None
4✔
1444
                blocks.append(constrained_primitive_block)
4✔
1445

1446
        elif isinstance(
4✔
1447
            our_type, (intermediate.AbstractClass, intermediate.ConcreteClass)
1448
        ):
1449
            # NOTE (mristin, 2022-10-01):
1450
            # We provide a general dispatch function for the most abstract
1451
            # class ``Class``.
1452
            pass
4✔
1453
        else:
1454
            assert_never(our_type)
×
1455

1456
    blocks.append(python_common.WARNING)
4✔
1457

1458
    if len(errors) > 0:
4✔
1459
        return None, errors
×
1460

1461
    writer = io.StringIO()
4✔
1462
    for i, block in enumerate(blocks):
4✔
1463
        if i > 0:
4✔
1464
            writer.write("\n\n\n")
4✔
1465

1466
        writer.write(block)
4✔
1467

1468
    writer.write("\n")
4✔
1469

1470
    return writer.getvalue(), None
4✔
1471

1472

1473
# endregion
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