Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions aas_core_codegen/cpp/transpilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,30 @@ def transform_is_in(
f"Please contact the developers if you need this feature.",
)

# NOTE (mristin):
# The membership in a string is a check for a sub-string, while
# ``common::Contains`` would look for a single character.
if (
intermediate_type_inference.try_primitive_type(container_type)
is intermediate_type_inference.PrimitiveType.STR
):
if not isinstance(node.container, parse_tree.Name):
container = Stripped(f"({container})")

one_liner = f"{container}.find({member}) != std::wstring::npos"
if "\n" not in one_liner and len(one_liner) <= 60:
return Stripped(one_liner), None

return (
Stripped(
f"""\
{container}.find(
{I}{indent_but_first_line(member, I)}
) != std::wstring::npos"""
),
None,
)

# NOTE (mristin):
# We look up the member in a set in constant time, while
# ``common::Contains`` iterates over all the items.
Expand All @@ -1177,6 +1201,12 @@ def transform_is_in(
None,
)

# NOTE (mristin):
# The lengths are ``size_t``'s, so we convert them to our integers to avoid
# comparing the signed and the unsigned integers in ``std::find``.
if isinstance(container_type, intermediate_type_inference.ListTypeAnnotation):
member = self._as_int64_position(node.member, member)

contains_function = cpp_naming.function_name(Identifier("contains"))

return (
Expand Down
16 changes: 16 additions & 0 deletions aas_core_codegen/golang/lib/_generate_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,22 @@ def generate(symbol_table: intermediate.SymbolTable) -> str:
func MapContains[K comparable, V any](m map[K]V, k K) bool {{
{I}_, ok := m[k]
{I}return ok
}}"""
),
Stripped(
f"""\
// Check if the slice contains the given value.
//
// We can not use ``slices.Contains`` as it is available only from Go 1.21 on.
// We compare the values as ``any``'s, since the interfaces, such as our classes,
// satisfy the constraint ``comparable`` only from Go 1.20 on.
func SliceContains[T any](s []T, v T) bool {{
{I}for _, item := range s {{
{II}if any(item) == any(v) {{
{III}return true
{II}}}
{I}}}
{I}return false
}}"""
),
Stripped(
Expand Down
40 changes: 38 additions & 2 deletions aas_core_codegen/golang/transpilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,8 +776,14 @@ def transform_is_in(

# NOTE (mristin):
# The lengths are ``int``'s, so we convert them to our integers to look
# them up in a set.
if isinstance(container_type, intermediate_type_inference.SetTypeAnnotation):
# them up in a set or a list.
if isinstance(
container_type,
(
intermediate_type_inference.SetTypeAnnotation,
intermediate_type_inference.ListTypeAnnotation,
),
):
member = self._as_int64_position(node.member, member)

# NOTE (mristin):
Expand All @@ -796,6 +802,36 @@ def transform_is_in(
ourcommon.MapContains(
{I}{indent_but_first_line(container, I)},
{I}{indent_but_first_line(member, I)},
)"""
),
None,
)

elif isinstance(container_type, intermediate_type_inference.ListTypeAnnotation):
return (
Stripped(
f"""\
ourcommon.SliceContains(
{I}{indent_but_first_line(container, I)},
{I}{indent_but_first_line(member, I)},
)"""
),
None,
)

elif (
intermediate_type_inference.try_primitive_type(container_type)
is intermediate_type_inference.PrimitiveType.STR
):
# NOTE (mristin):
# The native ``strings.Contains`` works on the UTF-8 bytes, which is
# equivalent to the check on the characters for a valid UTF-8.
return (
Stripped(
f"""\
strings.Contains(
{I}{indent_but_first_line(container, I)},
{I}{indent_but_first_line(member, I)},
)"""
),
None,
Expand Down
70 changes: 65 additions & 5 deletions aas_core_codegen/intermediate/type_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -3874,22 +3874,82 @@ def transform_is_in(self, node: parse_tree.IsIn) -> Optional["TypeAnnotationUnio
)
success = False

# NOTE (mristin):
# The membership in a JSON-able object is a question about its keys, so
# we restrict only the search in a list.
if isinstance(container_type, ListTypeAnnotation) and isinstance(
member_type,
(
JsonValueTypeAnnotation,
JsonArrayTypeAnnotation,
JsonObjectTypeAnnotation,
),
):
self.errors.append(
Error(
node.member.original_node,
f"We do not support searching for a JSON-able value in a list "
f"with ``in``, but the member is inferred to be {member_type}. "
f"The targets disagree on the comparison: Python compares "
f"the JSON-able values by value, while C#, Java and "
f"TypeScript compare the references of their arrays and "
f"objects, and Go panics when comparing them. Please "
f"contact the developers if you need this feature.",
)
)
success = False

if try_primitive_type(member_type) is PrimitiveType.BYTEARRAY:
self.errors.append(
Error(
node.member.original_node,
f"We do not support looking up a byte array with ``in``, "
f"but the member is inferred to be {member_type}. "
f"The targets disagree on the comparison: Python compares "
f"the byte arrays byte by byte, C# and Java compare "
f"the references of their ``byte[]``'s, while Go can not "
f"compare ``[]byte`` with ``==`` at all. Please compare "
f"the byte arrays explicitly instead, *e.g.*, in a for-loop.",
)
)
success = False

if not success:
return None

# NOTE (mristin):
# The targets look up the member in a hash set, so the member has to be of
# the same type as the items. We accept the constrained primitives in
# either direction, as they are represented by their constrainees.
if isinstance(container_type, SetTypeAnnotation) and not (
# The targets look up the member in a hash set or search for it in a list
# with the equality of the items, so the member has to be of the same type
# as the items. We accept the constrained primitives in either direction,
# as they are represented by their constrainees.
if isinstance(container_type, (SetTypeAnnotation, ListTypeAnnotation)) and not (
_assignable(target_type=container_type.items, value_type=member_type)
or _assignable(target_type=member_type, value_type=container_type.items)
):
what = "set" if isinstance(container_type, SetTypeAnnotation) else "list"

self.errors.append(
Error(
node.member.original_node,
f"Expected the member to be of the type of the items of "
f"the set {container_type}, but got: {member_type}",
f"the {what} {container_type}, but got: {member_type}",
)
)
return None

# NOTE (mristin):
# The membership in a string is a check for a sub-string, so the member
# has to be a string as well.
if (
try_primitive_type(container_type) is PrimitiveType.STR
and try_primitive_type(member_type) is not PrimitiveType.STR
):
self.errors.append(
Error(
node.member.original_node,
f"Expected the member to be a string as we check for "
f"a sub-string in the container {container_type}, "
f"but got: {member_type}",
)
)
return None
Expand Down
6 changes: 2 additions & 4 deletions aas_core_codegen/typescript/transpilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,10 +681,8 @@ def transform_is_in(
None,
)
elif (
isinstance(
container_type, intermediate_type_inference.PrimitiveTypeAnnotation
)
and container_type.a_type is intermediate_type_inference.PrimitiveType.STR
intermediate_type_inference.try_primitive_type(container_type)
is intermediate_type_inference.PrimitiveType.STR
):
return Stripped(f"{container}.includes({member})"), None
else:
Expand Down
36 changes: 36 additions & 0 deletions dev/test_data/common_meta_models/list_of_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,42 @@ def some_items_of(items: Sequence[Abstract_item]) -> List[Abstract_item]:
return result


@verification
def is_in_simples(simple: Simple, simples: Sequence[Simple]) -> bool:
"""
Check whether :paramref:`simple` is in :paramref:`simples`.

The targets compare the instances of the classes by their references.
"""
return simple in simples


@verification
def is_in_local_copy_of_simples(simple: Simple, simples: Sequence[Simple]) -> bool:
"""
Check whether :paramref:`simple` is in a local copy of :paramref:`simples`.

The local copy is built with ``append``, and holds the same instances as
:paramref:`simples`.
"""
copied: List[Simple] = []
for other in simples:
copied.append(other)

return simple in copied


@invariant(
lambda self: all(
is_in_local_copy_of_simples(simple, self.some_simples)
for simple in self.some_simples
),
"Each of some simples must be in a local copy of some simples.",
)
@invariant(
lambda self: not is_in_simples(self.item, self.some_simples),
"Item must not be one of some simples.",
)
@invariant(
lambda self: len(some_items_of(self.some_items)) <= 1,
"At most one of some items must be a some item.",
Expand Down
46 changes: 46 additions & 0 deletions dev/test_data/common_meta_models/list_of_constrained_primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

The list literal holds constrained primitives, while the appended items are
plain strings sliced from them.

We also check the membership ``in`` them. We look up a plain string in the list
of constrained primitives, a sliced string in a local list built with
``append``, and check for a sub-string in a constrained primitive.
"""
from typing import List, Sequence

Expand Down Expand Up @@ -34,6 +38,48 @@ def initials_of(names: Sequence[Name]) -> List[str]:
return initials


@verification
def initials_are_unique(names: Sequence[Name]) -> bool:
"""Check that :paramref:`names` start with unique characters."""
initials: List[str] = []
for name in names:
initial = name[:1]
if initial in initials:
return False

initials.append(initial)

return True


@verification
def is_in_names(text: str, names: Sequence[Name]) -> bool:
"""Check whether :paramref:`text` is in :paramref:`names`."""
return text in names


@verification
def contains_space(name: Name) -> bool:
"""Check whether :paramref:`name` contains a space."""
return " " in name


@invariant(
lambda self: initials_are_unique(self.some_names),
"Initials of some names must be unique.",
)
@invariant(
lambda self: all(not contains_space(name) for name in self.some_names),
"Some names must not contain spaces.",
)
@invariant(
lambda self: not is_in_names("Mallory", self.some_names),
"Some names must not contain Mallory.",
)
@invariant(
lambda self: not ("Eve" in self.some_names),
"Some names must not contain Eve.",
)
@invariant(
lambda self: all(initial != "x" for initial in initials_of(self.some_names)),
"Some names must not start with x.",
Expand Down
24 changes: 23 additions & 1 deletion dev/test_data/common_meta_models/list_of_enums.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
"""Check the lists of enumeration literals, their list literals and ``append``."""
"""
Check the lists of enumeration literals, their list literals and ``append``.

We also check the membership ``in`` a list of enumeration literals, passed as
an argument and given as a list literal.
"""
from enum import Enum
from typing import List, Sequence

Expand Down Expand Up @@ -31,6 +36,23 @@ def failures_of(results: Sequence[Result]) -> List[Result]:
return failures


@verification
def is_in_results(result: Result, results: Sequence[Result]) -> bool:
"""Check whether :paramref:`result` is in :paramref:`results`."""
return result in results


@invariant(
lambda self: all(
result in [Result.Ok, Result.Fail] for result in self.some_results
),
"Some results must be either ok or fail.",
)
@invariant(
lambda self: not is_in_results(Result.Fail, self.some_results)
or Result.Ok in self.some_results,
"Some results must contain an ok if they contain a failure.",
)
@invariant(
lambda self: len(failures_of(self.some_results)) <= 1,
"Some results must contain at most one failure.",
Expand Down
Loading
Loading