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

aas-core-works / aas-core-codegen / 28199159015

25 Jun 2026 08:41PM UTC coverage: 84.246% (+0.05%) from 84.197%
28199159015

push

github

web-flow
Introduce lists of constrained primitives (#656)

We implemented the support for lists of constrained primitives when we
introduced the lists of primitives, but we didn't test it.

In this change, we thoroughly test the lists of constrained primitives
and fix the generators where necessary.

6 of 7 new or added lines in 3 files covered. (85.71%)

1 existing line in 1 file now uncovered.

30391 of 36074 relevant lines covered (84.25%)

2.53 hits per line

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

86.43
/aas_core_codegen/python/lib/_generate_verification.py
1
"""Generate code of verification logic."""
2
import io
3✔
3
import textwrap
3✔
4
from typing import (
3✔
5
    Tuple,
6
    Optional,
7
    List,
8
    Sequence,
9
    Mapping,
10
    Union,
11
)
12

13
from icontract import ensure, require
3✔
14

15
from aas_core_codegen import intermediate, specific_implementations
3✔
16
from aas_core_codegen.common import (
3✔
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 (
3✔
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
3✔
32
from aas_core_codegen.parse import tree as parse_tree, retree as parse_retree
3✔
33
from aas_core_codegen.python import (
3✔
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(
3✔
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]
3✔
50

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

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

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

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

68
    return errors
×
69

70

71
# endregion
72

73
# region Generate
74

75

76
class _PatternVerificationTranspiler(
3✔
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))
3✔
82
    def _transform_joined_str_values(
3✔
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(
3✔
89
            isinstance(value, parse_tree.FormattedValue) for value in values
90
        )
91
        if not needs_interpolation:
3✔
92
            return (
3✔
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]
3✔
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
3✔
107
        single_quotes_count = 0
3✔
108

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

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

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

127
        for value in values:
3✔
128
            if isinstance(value, str):
3✔
129
                parts.append(
3✔
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):
3✔
139
                code, error = self.transform(value.value)
3✔
140
                if error is not None:
3✔
141
                    return None, error
×
142

143
                assert code is not None
3✔
144

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

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

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

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

162
    @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
3✔
163
    def transform_constant(
3✔
164
        self, node: parse_tree.Constant
165
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
166
        if isinstance(node.value, str):
3✔
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])
3✔
171
            if parse_error is not None:
3✔
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
3✔
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(
3✔
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))
3✔
200
    def transform_name(
3✔
201
        self, node: parse_tree.Name
202
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
203
        return Stripped(python_naming.variable_name(node.identifier)), None
3✔
204

205
    @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
3✔
206
    def transform_joined_str(
3✔
207
        self, node: parse_tree.JoinedStr
208
    ) -> Tuple[Optional[Stripped], Optional[Error]]:
209
        regex, parse_error = parse_retree.parse(values=node.values)
3✔
210
        if parse_error is not None:
3✔
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
3✔
226

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

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

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

243

244
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
3✔
245
def _transpile_pattern_verification(
3✔
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(
3✔
254
        Identifier(f"_construct_{verification.name}")
255
    )
256

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

259
    # region Construct block
260

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

269
    transpiler = _PatternVerificationTranspiler()
3✔
270

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

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

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

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

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

291
    # A pragmatic heuristics for breaking lines
292
    if len(pattern_expr) < 50:
3✔
293
        writer.write(textwrap.indent(f"return re.compile({pattern_expr})", I))
3✔
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()))
3✔
306

307
    # endregion
308

309
    # region Initialize the regex
310

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

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

315
    assert len(verification.arguments) == 1
3✔
316
    assert isinstance(
3✔
317
        verification.arguments[0].type_annotation, intermediate.PrimitiveTypeAnnotation
318
    )
319
    # noinspection PyUnresolvedReferences
320
    assert (
3✔
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)
3✔
326

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

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

336
    if verification.description is not None:
3✔
337
        (
3✔
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:
3✔
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
3✔
356

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

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

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

364
    # endregion
365

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

371
        writer.write(block)
3✔
372

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

375

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

379
    # fmt: off
380
    @require(
3✔
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__(
3✔
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__(
3✔
400
            self, type_map=type_map, environment=environment
401
        )
402

403
        self._symbol_table = symbol_table
3✔
404

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

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

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

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

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

423
        our_type = self._symbol_table.find_our_type(name=node.identifier)
3✔
424
        if isinstance(our_type, intermediate.Enumeration):
3✔
425
            return (
3✔
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(
3✔
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 = (
3✔
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:
3✔
457
        return None, error
×
458

459
    assert type_inference is not None
3✔
460

461
    transpiler = _TranspilableVerificationTranspiler(
3✔
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]
3✔
469
    for node in verification.parsed.body:
3✔
470
        stmt, error = transpiler.transform(node)
3✔
471
        if error is not None:
3✔
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
3✔
479
        body.append(stmt)
3✔
480

481
    writer = io.StringIO()
3✔
482

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

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

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

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

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

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

523
    docstring = None  # type: Optional[Stripped]
3✔
524
    if verification.description is not None:
3✔
525
        (
3✔
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:
3✔
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
3✔
544

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

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

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

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

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

559

560
class _InvariantTranspiler(python_transpilation.Transpiler):
3✔
561
    def __init__(
3✔
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__(
3✔
571
            self, type_map=type_map, environment=environment
572
        )
573

574
        self._symbol_table = symbol_table
3✔
575

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

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

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

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

593
        our_type = self._symbol_table.find_our_type(name=node.identifier)
3✔
594
        if isinstance(our_type, intermediate.Enumeration):
3✔
595
            return (
3✔
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))
3✔
611
def _transpile_invariant(
3✔
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 = (
3✔
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:
3✔
627
        return None, inference_error
×
628

629
    assert type_map is not None
3✔
630

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

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

641
    assert expr is not None
3✔
642

643
    writer = io.StringIO()
3✔
644
    if len(expr) > 50 or "\n" in expr:
3✔
645
        writer.write("if not (\n")
3✔
646
        writer.write(textwrap.indent(expr, I))
3✔
647
        writer.write("\n):\n")
3✔
648
    else:
649
        no_parenthesis_type_in_this_context = (
3✔
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):
3✔
658
            not_expr = f"not {expr}"
3✔
659
        else:
660
            not_expr = f"not ({expr})"
3✔
661

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

664
    writer.write(f"{I}yield Error(\n")
3✔
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)
3✔
670

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

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

680

681
OurTypeExceptEnumeration = Union[
3✔
682
    intermediate.ConstrainedPrimitive,
683
    intermediate.AbstractClass,
684
    intermediate.ConcreteClass,
685
]
686
assert_union_without_excluded(
3✔
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))
3✔
694
def _generate_verify_property_snippet(
3✔
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 = (
3✔
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):
3✔
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):
3✔
730
        if isinstance(type_anno.items, intermediate.OptionalTypeAnnotation):
3✔
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):
3✔
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]
3✔
752

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

756
    if isinstance(type_anno, intermediate.PrimitiveTypeAnnotation):
3✔
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
3✔
760
    elif isinstance(type_anno, intermediate.OurTypeAnnotation):
3✔
761
        if isinstance(type_anno.our_type, intermediate.Enumeration):
3✔
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
3✔
765

766
        elif isinstance(type_anno.our_type, intermediate.ConstrainedPrimitive):
3✔
767
            function_name = python_naming.function_name(
3✔
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})"
3✔
772
            # Heuristic to break the lines, very rudimentary
773
            if len(for_error_in_verify) > 70:
3✔
774
                for_error_in_verify = f"""\
3✔
775
for error in {function_name}(
776
{II}that.{prop_name}
777
)"""
778

779
            stmts.append(
3✔
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(
3✔
794
            type_anno.our_type, (intermediate.AbstractClass, intermediate.ConcreteClass)
795
        ):
796
            for_error_in_self_transform = (
3✔
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:
3✔
801
                for_error_in_self_transform = f"""\
×
802
for error in self.transform(
803
{II}that.{prop_name}
804
)"""
805

806
            stmts.append(
3✔
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):
3✔
823
        assert not isinstance(
3✔
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
        loop_variable = next(generator_for_loop_variables)
3✔
832

833
        for_error: Stripped
834

835
        if isinstance(type_anno.items, intermediate.PrimitiveTypeAnnotation):
3✔
836
            # NOTE (mristin):
837
            # There is nothing to verify about the primitive types.
838
            return Stripped(""), None
3✔
839

840
        elif isinstance(type_anno.items, intermediate.OurTypeAnnotation):
3✔
841
            if isinstance(type_anno.items.our_type, intermediate.Enumeration):
3✔
842
                # NOTE (mristin):
843
                # There is nothing to verify about the enumeration.
844
                return Stripped(""), None
3✔
845

846
            elif isinstance(
3✔
847
                type_anno.items.our_type, intermediate.ConstrainedPrimitive
848
            ):
849
                function_name = python_naming.function_name(
3✔
850
                    Identifier(f"verify_{type_anno.items.our_type.name}")
851
                )
852

853
                for_error = Stripped(f"for error in {function_name}({loop_variable})")
3✔
854

855
                # Heuristic to break the lines, very rudimentary
856
                if len(for_error) > 70:
3✔
NEW
857
                    for_error = Stripped(
×
858
                        f"""\
859
for error in {function_name}(
860
{II}{loop_variable}
861
)"""
862
                    )
863

864
            elif isinstance(
3✔
865
                type_anno.items.our_type,
866
                (intermediate.AbstractClass, intermediate.ConcreteClass),
867
            ):
868
                for_error = Stripped(
3✔
869
                    f"""for error in self.transform({loop_variable})"""
870
                )
871

872
                if len(for_error) > 70:
3✔
873
                    for_error = Stripped(
×
874
                        f"""\
875
for error in self.transform(
876
{II}{loop_variable}
877
)"""
878
                    )
879

880
            else:
881
                # noinspection PyTypeChecker
882
                assert_never(type_anno.items.our_type)
×
883

884
        else:
885
            # noinspection PyTypeChecker
886
            assert_never(type_anno.items)
×
887

888
        for_i_item_in_that_prop = (
3✔
889
            f"for i, {loop_variable} in enumerate(that.{prop_name})"
890
        )
891

892
        # Rudimentary heuristics for line breaking
893
        if len(for_i_item_in_that_prop) > 70:
3✔
894
            for_i_item_in_that_prop = f"""\
3✔
895
for i, {loop_variable} in enumerate(
896
{II}that.{prop_name}
897
)"""
898

899
        stmts.append(
3✔
900
            Stripped(
901
                f"""\
902
{for_i_item_in_that_prop}:
903
{I}{indent_but_first_line(for_error, I)}:
904
{II}error.path._prepend(
905
{III}IndexSegment(
906
{IIII}that.{prop_name},
907
{IIII}i
908
{III})
909
{II})
910
{II}error.path._prepend(
911
{III}PropertySegment(
912
{IIII}that,
913
{IIII}{prop_name_literal}
914
{III})
915
{II})
916
{II}yield error"""
917
            )
918
        )
919

920
    else:
921
        assert_never(type_anno)
×
922

923
    verify_block = Stripped("\n".join(stmts))
3✔
924

925
    if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation):
3✔
926
        return (
3✔
927
            Stripped(
928
                f"""\
929
if that.{prop_name} is not None:
930
{I}{indent_but_first_line(verify_block, I)}"""
931
            ),
932
            None,
933
        )
934

935
    return verify_block, None
3✔
936

937

938
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
3✔
939
def _generate_transform_for_class(
3✔
940
    cls: intermediate.ConcreteClass,
941
    symbol_table: intermediate.SymbolTable,
942
    base_environment: intermediate_type_inference.Environment,
943
) -> Tuple[Optional[Stripped], Optional[List[Error]]]:
944
    """Generate the transform method to errors for the given concrete class."""
945
    errors = []  # type: List[Error]
3✔
946
    blocks = []  # type: List[Stripped]
3✔
947

948
    environment = intermediate_type_inference.MutableEnvironment(
3✔
949
        parent=base_environment
950
    )
951

952
    assert environment.find(Identifier("self")) is None
3✔
953
    environment.set(
3✔
954
        identifier=Identifier("self"),
955
        type_annotation=intermediate_type_inference.OurTypeAnnotation(our_type=cls),
956
    )
957

958
    for invariant in cls.invariants:
3✔
959
        invariant_code, error = _transpile_invariant(
3✔
960
            invariant=invariant, symbol_table=symbol_table, environment=environment
961
        )
962
        if error is not None:
3✔
963
            errors.append(
×
964
                Error(
965
                    cls.parsed.node,
966
                    f"Failed to transpile the invariant of the class {cls.name!r}",
967
                    [error],
968
                )
969
            )
970
            continue
×
971

972
        assert invariant_code is not None
3✔
973

974
        blocks.append(invariant_code)
3✔
975

976
    if len(errors) > 0:
3✔
977
        return None, errors
×
978

979
    # NOTE (mristin, 2022-10-14):
980
    # We need to generate unique loop variable for each loop since Python tracks
981
    # the variables in function scope, not block scope.
982
    generator_for_loop_variables = python_common.GeneratorForLoopVariables()
3✔
983

984
    for prop in cls.properties:
3✔
985
        block, error = _generate_verify_property_snippet(
3✔
986
            prop=prop, generator_for_loop_variables=generator_for_loop_variables
987
        )
988
        if error is not None:
3✔
989
            errors.append(error)
×
990
        else:
991
            assert block is not None
3✔
992
            if block != "":
3✔
993
                blocks.append(block)
3✔
994

995
    if len(errors) > 0:
3✔
996
        return None, errors
×
997

998
    cls_name = python_naming.class_name(cls.name)
3✔
999

1000
    if len(blocks) == 0:
3✔
1001
        blocks.append(
3✔
1002
            Stripped(
1003
                f"""\
1004
# No verification has been defined for {cls_name}.
1005
return
1006
# For this uncommon return-yield construction, see:
1007
# https://stackoverflow.com/questions/13243766/how-to-define-an-empty-generator-function
1008
# noinspection PyUnreachableCode
1009
yield"""
1010
            )
1011
        )
1012

1013
    transform_name = python_naming.method_name(Identifier(f"transform_{cls.name}"))
3✔
1014

1015
    writer = io.StringIO()
3✔
1016
    writer.write(
3✔
1017
        f"""\
1018
# noinspection PyMethodMayBeStatic
1019
def {transform_name}(
1020
{II}self,
1021
{II}that: aas_types.{cls_name}
1022
) -> Iterator[Error]:
1023
"""
1024
    )
1025

1026
    for i, stmt in enumerate(blocks):
3✔
1027
        if i > 0:
3✔
1028
            writer.write("\n\n")
3✔
1029
        writer.write(textwrap.indent(stmt, I))
3✔
1030

1031
    return Stripped(writer.getvalue()), None
3✔
1032

1033

1034
def _generate_transformer(
3✔
1035
    symbol_table: intermediate.SymbolTable,
1036
    base_environment: intermediate_type_inference.Environment,
1037
    spec_impls: specific_implementations.SpecificImplementations,
1038
) -> Tuple[Optional[Stripped], Optional[List[Error]]]:
1039
    """Generate a transformer to double-dispatch an instance to errors."""
1040
    errors = []  # type: List[Error]
3✔
1041

1042
    blocks = []  # type: List[Stripped]
3✔
1043

1044
    # The abstract classes are directly dispatched by the transformer,
1045
    # so we do not need to handle them separately.
1046

1047
    for cls in symbol_table.concrete_classes:
3✔
1048
        if cls.is_implementation_specific:
3✔
1049
            transform_key = specific_implementations.ImplementationKey(
×
1050
                f"Verification/transform_{cls.name}.py"
1051
            )
1052

1053
            implementation = spec_impls.get(transform_key, None)
×
1054
            if implementation is None:
×
1055
                errors.append(
×
1056
                    Error(
1057
                        cls.parsed.node,
1058
                        f"The transformation snippet is missing "
1059
                        f"for the implementation-specific "
1060
                        f"class {cls.name}: {transform_key}",
1061
                    )
1062
                )
1063
                continue
×
1064

1065
            blocks.append(spec_impls[transform_key])
×
1066
        else:
1067
            block, cls_errors = _generate_transform_for_class(
3✔
1068
                cls=cls,
1069
                symbol_table=symbol_table,
1070
                base_environment=base_environment,
1071
            )
1072
            if cls_errors is not None:
3✔
1073
                errors.extend(cls_errors)
×
1074
            else:
1075
                assert block is not None
3✔
1076
                blocks.append(block)
3✔
1077

1078
    if len(errors) > 0:
3✔
1079
        return None, errors
×
1080

1081
    writer = io.StringIO()
3✔
1082
    writer.write(
3✔
1083
        f"""\
1084
class _Transformer(
1085
{II}aas_types.AbstractTransformer[
1086
{III}Iterator[Error]
1087
{II}]
1088
):
1089
"""
1090
    )
1091

1092
    for i, block in enumerate(blocks):
3✔
1093
        if i > 0:
3✔
1094
            writer.write("\n\n")
3✔
1095
        writer.write(textwrap.indent(block, I))
3✔
1096

1097
    return Stripped(writer.getvalue()), None
3✔
1098

1099

1100
def _generate_verify_constrained_primitive(
3✔
1101
    constrained_primitive: intermediate.ConstrainedPrimitive,
1102
    symbol_table: intermediate.SymbolTable,
1103
    base_environment: intermediate_type_inference.Environment,
1104
) -> Tuple[Optional[Stripped], Optional[List[Error]]]:
1105
    """Generate the verify function for the constrained primitives."""
1106
    errors = []  # type: List[Error]
3✔
1107
    blocks = []  # type: List[Stripped]
3✔
1108

1109
    environment = intermediate_type_inference.MutableEnvironment(
3✔
1110
        parent=base_environment
1111
    )
1112

1113
    assert environment.find(Identifier("self")) is None
3✔
1114
    environment.set(
3✔
1115
        identifier=Identifier("self"),
1116
        type_annotation=intermediate_type_inference.OurTypeAnnotation(
1117
            our_type=constrained_primitive
1118
        ),
1119
    )
1120

1121
    for invariant in constrained_primitive.invariants:
3✔
1122
        invariant_code, error = _transpile_invariant(
3✔
1123
            invariant=invariant, symbol_table=symbol_table, environment=environment
1124
        )
1125
        if error is not None:
3✔
1126
            errors.append(
×
1127
                Error(
1128
                    constrained_primitive.parsed.node,
1129
                    f"Failed to transpile the invariant of "
1130
                    f"the constrained primitive {constrained_primitive.name!r}",
1131
                    [error],
1132
                )
1133
            )
1134
            continue
×
1135

1136
        assert invariant_code is not None
3✔
1137

1138
        blocks.append(invariant_code)
3✔
1139

1140
    if len(errors) > 0:
3✔
1141
        return None, errors
×
1142

1143
    no_verification_specified = False
3✔
1144
    if len(blocks) == 0:
3✔
1145
        no_verification_specified = True
3✔
1146
        blocks.append(
3✔
1147
            Stripped(
1148
                """\
1149
# There is no verification specified.
1150
return
1151

1152
# Empty generator according to:
1153
# https://stackoverflow.com/a/13243870/1600678
1154
# noinspection PyUnreachableCode
1155
yield"""
1156
            )
1157
        )
1158

1159
    function_name = python_naming.function_name(
3✔
1160
        Identifier(f"verify_{constrained_primitive.name}")
1161
    )
1162

1163
    that_type = python_common.PRIMITIVE_TYPE_MAP[constrained_primitive.constrainee]
3✔
1164

1165
    writer = io.StringIO()
3✔
1166

1167
    if no_verification_specified:
3✔
1168
        # NOTE (mristin, 2022-10-02):
1169
        # We provide a function for evolvability even though it does nothing.
1170
        writer.write("# noinspection PyUnusedLocal\n")
3✔
1171

1172
    writer.write(
3✔
1173
        f"""\
1174
def {function_name}(
1175
{II}that: {that_type}
1176
) -> Iterator[Error]:
1177
{I}\"\"\"Verify the constraints of :paramref:`that`.\"\"\"
1178
"""
1179
    )
1180

1181
    for i, block in enumerate(blocks):
3✔
1182
        if i > 0:
3✔
1183
            writer.write("\n\n")
3✔
1184
        writer.write(textwrap.indent(block, I))
3✔
1185

1186
    assert len(errors) == 0
3✔
1187
    return Stripped(writer.getvalue()), None
3✔
1188

1189

1190
def _generate_module_docstring(
3✔
1191
    symbol_table: intermediate.SymbolTable,
1192
    qualified_module_name: python_common.QualifiedModuleName,
1193
) -> Stripped:
1194
    """Generate the docstring for the module."""
1195
    docstring_blocks = [
3✔
1196
        Stripped("Verify that the instances of the meta-model satisfy the invariants.")
1197
    ]  # type: List[Stripped]
1198

1199
    first_cls = (
3✔
1200
        symbol_table.concrete_classes[0]
1201
        if len(symbol_table.concrete_classes) > 0
1202
        else None
1203
    )  # type: Optional[intermediate.ConcreteClass]
1204

1205
    if first_cls is not None:
3✔
1206
        cls_name = python_naming.class_name(first_cls.name)
3✔
1207
        an_instance_variable = python_naming.variable_name(Identifier("an_instance"))
3✔
1208

1209
        docstring_blocks.append(
3✔
1210
            Stripped(
1211
                f"""\
1212
Here is an example how to verify an instance of :py:class:`{qualified_module_name}.types.{cls_name}`:
1213

1214
.. code-block::
1215

1216
    import {qualified_module_name}.types as aas_types
1217
    import {qualified_module_name}.verification as aas_verification
1218

1219
    {an_instance_variable} = aas_types.{cls_name}(
1220
        # ... some constructor arguments ...
1221
    )
1222

1223
    for error in aas_verification.verify({an_instance_variable}):
1224
        print(f"{{error.cause}} at: {{error.path}}")"""
1225
            )
1226
        )
1227

1228
    # endregion
1229

1230
    if len(docstring_blocks) == 1:
3✔
1231
        doc_escaped = docstring_blocks[0].replace('"""', '\\"\\"\\"')
×
1232
        docstring = f'"""{doc_escaped}"""'
×
1233
    else:
1234
        doc_escaped = ("\n\n".join(docstring_blocks)).replace('"""', '\\"\\"\\"')
3✔
1235
        docstring = f'''\
3✔
1236
"""
1237
{doc_escaped}
1238
"""'''
1239

1240
    return Stripped(docstring)
3✔
1241

1242

1243
# fmt: off
1244
@ensure(lambda result: (result[0] is not None) ^ (result[1] is not None))
3✔
1245
@ensure(
3✔
1246
    lambda result:
1247
    not (result[0] is not None) or result[0].endswith('\n'),
1248
    "Trailing newline mandatory for valid end-of-files"
1249
)
1250
# fmt: on
1251
def generate(
3✔
1252
    symbol_table: intermediate.SymbolTable,
1253
    qualified_module_name: python_common.QualifiedModuleName,
1254
    spec_impls: specific_implementations.SpecificImplementations,
1255
) -> Tuple[Optional[str], Optional[List[Error]]]:
1256
    """
1257
    Generate code of verification logic.
1258

1259
    The ``qualified_module_name`` indicates the fully-qualified name of the base module.
1260
    """
1261
    # region Module docstring
1262
    blocks = [
3✔
1263
        _generate_module_docstring(
1264
            symbol_table=symbol_table, qualified_module_name=qualified_module_name
1265
        ),
1266
        python_common.WARNING,
1267
        Stripped(
1268
            f"""\
1269
import math
1270
import re
1271
import struct
1272
import sys
1273
from typing import (
1274
{I}Any,
1275
{I}Callable,
1276
{I}Iterable,
1277
{I}Iterator,
1278
{I}List,
1279
{I}Mapping,
1280
{I}Optional,
1281
{I}Pattern,
1282
{I}Sequence,
1283
{I}Set,
1284
{I}Union
1285
)
1286

1287
if sys.version_info >= (3, 8):
1288
{I}from typing import Final
1289
else:
1290
{I}from typing_extensions import Final
1291

1292
from {qualified_module_name} import (
1293
{I}constants as aas_constants,
1294
{I}types as aas_types,
1295
)"""
1296
        ),
1297
        Stripped(
1298
            f"""\
1299
class PropertySegment:
1300
{I}\"\"\"Represent a property access on a path to an erroneous value.\"\"\"
1301

1302
{I}#: Instance containing the property
1303
{I}instance: Final[aas_types.Class]
1304

1305
{I}#: Name of the property
1306
{I}name: Final[str]
1307

1308
{I}def __init__(
1309
{III}self,
1310
{III}instance: aas_types.Class,
1311
{III}name: str
1312
{I}) -> None:
1313
{II}\"\"\"Initialize with the given values.\"\"\"
1314
{II}self.instance = instance
1315
{II}self.name = name
1316

1317
{I}def __str__(self) -> str:
1318
{II}return f'.{{self.name}}'"""
1319
        ),
1320
        Stripped(
1321
            f"""\
1322
class IndexSegment:
1323
{I}\"\"\"Represent an index access on a path to an erroneous value.\"\"\"
1324

1325
{I}#: Sequence containing the item at :py:attr:`~index`
1326
{I}sequence: Final[Sequence[Any]]
1327

1328
{I}#: Index of the item
1329
{I}index: Final[int]
1330

1331
{I}def __init__(
1332
{III}self,
1333
{III}sequence: Sequence[Any],
1334
{III}index: int
1335
{I}) -> None:
1336
{II}\"\"\"Initialize with the given values.\"\"\"
1337
{II}self.sequence = sequence
1338
{II}self.index = index
1339

1340
{I}def __str__(self) -> str:
1341
{II}return f'[{{self.index}}]'"""
1342
        ),
1343
        Stripped("Segment = Union[PropertySegment, IndexSegment]"),
1344
        Stripped(
1345
            f"""\
1346
class Path:
1347
{I}\"\"\"Represent the relative path to the erroneous value.\"\"\"
1348

1349
{I}def __init__(self) -> None:
1350
{II}\"\"\"Initialize as an empty path.\"\"\"
1351
{II}self._segments = []  # type: List[Segment]
1352

1353
{I}@property
1354
{I}def segments(self) -> Sequence[Segment]:
1355
{II}\"\"\"Get the segments of the path.\"\"\"
1356
{II}return self._segments
1357

1358
{I}def _prepend(self, segment: Segment) -> None:
1359
{II}\"\"\"Insert the :paramref:`segment` in front of other segments.\"\"\"
1360
{II}self._segments.insert(0, segment)
1361

1362
{I}def __str__(self) -> str:
1363
{II}return "".join(str(segment) for segment in self._segments)"""
1364
        ),
1365
        Stripped(
1366
            f"""\
1367
class Error:
1368
{I}\"\"\"Represent a verification error in the data.\"\"\"
1369

1370
{I}#: Human-readable description of the error
1371
{I}cause: Final[str]
1372

1373
{I}#: Path to the erroneous value
1374
{I}path: Final[Path]
1375

1376
{I}def __init__(self, cause: str) -> None:
1377
{II}\"\"\"Initialize as an error with an empty path.\"\"\"
1378
{II}self.cause = cause
1379
{II}self.path = Path()
1380

1381
{I}def __repr__(self) -> str:
1382
{II}return f"Error(path={{self.path}}, cause={{self.cause}})\""""
1383
        ),
1384
    ]  # type: List[Stripped]
1385

1386
    errors = []  # type: List[Error]
3✔
1387

1388
    base_environment = intermediate_type_inference.populate_base_environment(
3✔
1389
        symbol_table=symbol_table
1390
    )
1391

1392
    for verification in symbol_table.verification_functions:
3✔
1393
        if isinstance(verification, intermediate.ImplementationSpecificVerification):
3✔
1394
            implementation_key = specific_implementations.ImplementationKey(
3✔
1395
                f"Verification/{verification.name}.py"
1396
            )
1397

1398
            implementation = spec_impls.get(implementation_key, None)
3✔
1399
            if implementation is None:
3✔
1400
                errors.append(
×
1401
                    Error(
1402
                        None,
1403
                        f"The snippet for the verification function "
1404
                        f"{verification.name!r} is missing: {implementation_key}",
1405
                    )
1406
                )
1407
            else:
1408
                blocks.append(implementation)
3✔
1409

1410
        elif isinstance(verification, intermediate.PatternVerification):
3✔
1411
            implementation, error = _transpile_pattern_verification(
3✔
1412
                verification=verification, qualified_module_name=qualified_module_name
1413
            )
1414

1415
            if error is not None:
3✔
1416
                errors.append(error)
×
1417
            else:
1418
                assert implementation is not None
3✔
1419
                blocks.append(implementation)
3✔
1420

1421
        elif isinstance(verification, intermediate.TranspilableVerification):
3✔
1422
            implementation, error = _transpile_transpilable_verification(
3✔
1423
                verification=verification,
1424
                symbol_table=symbol_table,
1425
                environment=base_environment,
1426
                qualified_module_name=qualified_module_name,
1427
            )
1428

1429
            if error is not None:
3✔
1430
                errors.append(error)
×
1431
            else:
1432
                assert implementation is not None
3✔
1433
                blocks.append(implementation)
3✔
1434

1435
        else:
1436
            assert_never(verification)
×
1437

1438
    transformer_block, transformer_errors = _generate_transformer(
3✔
1439
        symbol_table=symbol_table,
1440
        base_environment=base_environment,
1441
        spec_impls=spec_impls,
1442
    )
1443
    if transformer_errors is not None:
3✔
1444
        errors.extend(transformer_errors)
×
1445
    else:
1446
        assert transformer_block is not None
3✔
1447
        blocks.append(transformer_block)
3✔
1448

1449
    blocks.append(Stripped("_TRANSFORMER = _Transformer()"))
3✔
1450

1451
    blocks.append(
3✔
1452
        Stripped(
1453
            f"""\
1454
def verify(
1455
{II}that: aas_types.Class
1456
) -> Iterator[Error]:
1457
{I}\"\"\"
1458
{I}Verify the constraints of :paramref:`that` recursively.
1459

1460
{I}:param that: instance whose constraints we want to verify
1461
{I}:yield: constraint violations
1462
{I}\"\"\"
1463
{I}yield from _TRANSFORMER.transform(that)"""
1464
        )
1465
    )
1466

1467
    for our_type in symbol_table.our_types:
3✔
1468
        if isinstance(our_type, intermediate.Enumeration):
3✔
1469
            # NOTE (mristin, 2022-10-01):
1470
            # We do not verify the enumerations explicitly in Python as mypy
1471
            # is capable enough to spot invalid enum literals.
1472
            pass
3✔
1473

1474
        elif isinstance(our_type, intermediate.ConstrainedPrimitive):
3✔
1475
            (
3✔
1476
                constrained_primitive_block,
1477
                constrained_primitive_errors,
1478
            ) = _generate_verify_constrained_primitive(
1479
                constrained_primitive=our_type,
1480
                symbol_table=symbol_table,
1481
                base_environment=base_environment,
1482
            )
1483

1484
            if constrained_primitive_errors is not None:
3✔
1485
                errors.extend(constrained_primitive_errors)
×
1486
            else:
1487
                assert constrained_primitive_block is not None
3✔
1488
                blocks.append(constrained_primitive_block)
3✔
1489

1490
        elif isinstance(
3✔
1491
            our_type, (intermediate.AbstractClass, intermediate.ConcreteClass)
1492
        ):
1493
            # NOTE (mristin, 2022-10-01):
1494
            # We provide a general dispatch function for the most abstract
1495
            # class ``Class``.
1496
            pass
3✔
1497
        else:
1498
            assert_never(our_type)
×
1499

1500
    blocks.append(python_common.WARNING)
3✔
1501

1502
    if len(errors) > 0:
3✔
1503
        return None, errors
×
1504

1505
    writer = io.StringIO()
3✔
1506
    for i, block in enumerate(blocks):
3✔
1507
        if i > 0:
3✔
1508
            writer.write("\n\n\n")
3✔
1509

1510
        writer.write(block)
3✔
1511

1512
    writer.write("\n")
3✔
1513

1514
    return writer.getvalue(), None
3✔
1515

1516

1517
# endregion
1518

1519
assert generate.__doc__ is not None
3✔
1520
assert generate.__doc__.strip().startswith(__doc__.strip())
3✔
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