diff --git a/changelogs/unreleased/4982-implicit-relation-inverse.yml b/changelogs/unreleased/4982-implicit-relation-inverse.yml new file mode 100644 index 0000000000..8e4db03dc0 --- /dev/null +++ b/changelogs/unreleased/4982-implicit-relation-inverse.yml @@ -0,0 +1,9 @@ +description: Added implicit inverse relation assignment for bidirectional relations with an index +change-type: minor +sections: + feature: + Constructors that appear as a right hand side in an assignment (or another constructor) now no longer require + explicit assignments for the inverse relation to the left hand side. +destination-branches: + - iso5 + - master diff --git a/src/inmanta/__init__.py b/src/inmanta/__init__.py index b1da629072..0ed93d3e1a 100644 --- a/src/inmanta/__init__.py +++ b/src/inmanta/__init__.py @@ -16,7 +16,7 @@ Contact: code@inmanta.com """ -COMPILER_VERSION = "2022.4" +COMPILER_VERSION = "2022.5" RUNNING_TESTS = False """ This is enabled/disabled by the test suite when tests are run. diff --git a/src/inmanta/ast/constraint/expression.py b/src/inmanta/ast/constraint/expression.py index bf40709275..00bdef37da 100644 --- a/src/inmanta/ast/constraint/expression.py +++ b/src/inmanta/ast/constraint/expression.py @@ -26,6 +26,7 @@ from inmanta import stable_api from inmanta.ast import LocatableString, RuntimeException, TypingException from inmanta.ast.statements import ( + AttributeAssignmentLHS, ExpressionStatement, Literal, ReferenceStatement, @@ -236,7 +237,7 @@ class LazyBooleanOperator(BinaryOperator, Resumer): def __init__(self, name: str, op1: ExpressionStatement, op2: ExpressionStatement) -> None: Operator.__init__(self, name, [op1, op2]) - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: super().normalize() # lazy execution: we don't immediately emit the second operator so we need to hold its promises until we do self._own_eager_promises = list(self.children[1].get_all_eager_promises()) diff --git a/src/inmanta/ast/entity.py b/src/inmanta/ast/entity.py index 8ae6672614..a4a7f3cb55 100644 --- a/src/inmanta/ast/entity.py +++ b/src/inmanta/ast/entity.py @@ -227,7 +227,7 @@ def set_attributes(self, attributes: "Dict[str,Attribute]") -> None: def is_parent(self, entity: "Entity") -> bool: """ - Check if the given entity is a parent of this entity + Check if the given entity is a parent of this entity. Does not consider an entity its own parent. """ if entity in self.parent_entities: return True @@ -337,7 +337,7 @@ def get_instance( def is_subclass(self, cls: "Entity") -> bool: """ - Is the given class a subclass of this class + Is the given class a subclass of this class. Does not consider entities a subclass of themselves. """ return cls.is_parent(self) diff --git a/src/inmanta/ast/statements/__init__.py b/src/inmanta/ast/statements/__init__.py index c9f48a4df5..24caf5af7d 100644 --- a/src/inmanta/ast/statements/__init__.py +++ b/src/inmanta/ast/statements/__init__.py @@ -201,9 +201,24 @@ def _fulfill_promises(self, requires: Dict[object, object]) -> None: promise.fulfill() +@dataclass(frozen=True) +class AttributeAssignmentLHS: + instance: "Reference" + attribute: str + + class ExpressionStatement(RequiresEmitStatement): __slots__ = () + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: + """ + :param lhs_attribute: The left hand side attribute if this expression is a right hand side in an attribute assignment. + If not None, that caller is responsible for making sure the reference resolves to the correct instance as soon as + this statement enters the `requires_emit` stage. As a result, it should always be None if the instance construction + depends on this statement. + """ + raise NotImplementedError() + def as_constant(self) -> object: """ Returns this expression as a constant value, if possible. Otherwise, raise a RuntimeException. @@ -462,7 +477,7 @@ def __init__(self, children: List[ExpressionStatement]) -> None: self.children: Sequence[ExpressionStatement] = children self.anchors.extend((anchor for e in self.children for anchor in e.get_anchors())) - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: for c in self.children: c.normalize() @@ -523,7 +538,7 @@ def __init__(self, value: object) -> None: self.value = value self.lexpos: Optional[int] = None - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: pass def __repr__(self) -> str: diff --git a/src/inmanta/ast/statements/assign.py b/src/inmanta/ast/statements/assign.py index 92143a90a0..d67b48aa9b 100644 --- a/src/inmanta/ast/statements/assign.py +++ b/src/inmanta/ast/statements/assign.py @@ -40,6 +40,7 @@ from inmanta.ast.attribute import RelationAttribute from inmanta.ast.statements import ( AssignStatement, + AttributeAssignmentLHS, ExpressionStatement, RequiresEmitStatement, Resumer, @@ -86,6 +87,11 @@ def __init__(self, items: typing.List[ExpressionStatement]) -> None: ReferenceStatement.__init__(self, items) self.items = items + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: + for item in self.items: + # pass on lhs_attribute to children + item.normalize(lhs_attribute=lhs_attribute) + def requires_emit_gradual( self, resolver: Resolver, queue: QueueScheduler, resultcollector: Optional[ResultCollector] ) -> typing.Dict[object, VariableABC]: @@ -220,6 +226,10 @@ def __init__(self, instance: "Reference", attribute_name: str, value: Expression self.list_only = list_only self._assignment_promise: StaticEagerPromise = StaticEagerPromise(self.instance, self.attribute_name, self) + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: + # register this assignment as left hand side to the value on the right hand side + self.rhs.normalize(lhs_attribute=AttributeAssignmentLHS(self.instance, self.attribute_name)) + def get_all_eager_promises(self) -> Iterator["StaticEagerPromise"]: # propagate this attribute assignment's promise to parent blocks return chain(super().get_all_eager_promises(), [self._assignment_promise]) @@ -433,7 +443,7 @@ def __init__( self.query = [(str(n), e) for n, e in query] self.wrapped_query: typing.List["WrappedKwargs"] = wrapped_query - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: ReferenceStatement.normalize(self) self.type = self.namespace.get_type(self.index_type) @@ -497,7 +507,7 @@ def __init__( self.querypart: typing.List[typing.Tuple[str, ExpressionStatement]] = [(str(n), e) for n, e in query] self.wrapped_querypart: typing.List["WrappedKwargs"] = wrapped_query - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: ReferenceStatement.normalize(self) # currently there is no way to get the type of an expression prior to evaluation self.type = None diff --git a/src/inmanta/ast/statements/call.py b/src/inmanta/ast/statements/call.py index 2aad162408..2dba382f82 100644 --- a/src/inmanta/ast/statements/call.py +++ b/src/inmanta/ast/statements/call.py @@ -33,7 +33,7 @@ TypeReferenceAnchor, WrappingRuntimeException, ) -from inmanta.ast.statements import ExpressionStatement, ReferenceStatement +from inmanta.ast.statements import AttributeAssignmentLHS, ExpressionStatement, ReferenceStatement from inmanta.ast.statements.generator import WrappedKwargs from inmanta.execute.dataflow import DataflowGraph from inmanta.execute.proxy import UnknownException, UnsetException @@ -81,7 +81,7 @@ def __init__( self.kwargs[arg_name] = expr self.function: Optional[Function] = None - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: ReferenceStatement.normalize(self) func = self.namespace.get_type(self.name) if isinstance(func, InmantaType.Primitive): diff --git a/src/inmanta/ast/statements/generator.py b/src/inmanta/ast/statements/generator.py index 39bfbadc71..378aaadd95 100644 --- a/src/inmanta/ast/statements/generator.py +++ b/src/inmanta/ast/statements/generator.py @@ -19,6 +19,8 @@ # pylint: disable-msg=W0613,R0201 import logging +import uuid +from collections import abc from itertools import chain from typing import Dict, Iterator, List, Optional, Set, Tuple @@ -32,14 +34,22 @@ Location, Namespace, NotFoundException, + Range, RuntimeException, TypeReferenceAnchor, TypingException, ) from inmanta.ast.attribute import Attribute, RelationAttribute from inmanta.ast.blocks import BasicBlock -from inmanta.ast.statements import ExpressionStatement, RawResumer, RequiresEmitStatement, StaticEagerPromise +from inmanta.ast.statements import ( + AttributeAssignmentLHS, + ExpressionStatement, + RawResumer, + RequiresEmitStatement, + StaticEagerPromise, +) from inmanta.ast.statements.assign import GradualSetAttributeHelper, SetAttributeHelper +from inmanta.ast.variables import Reference from inmanta.const import LOG_LEVEL_TRACE from inmanta.execute.dataflow import DataflowGraph from inmanta.execute.runtime import ( @@ -51,6 +61,7 @@ ResultCollector, ResultVariable, VariableABC, + VariableResolver, WrappedValueVariable, ) from inmanta.execute.tracking import ImplementsTracker @@ -84,7 +95,7 @@ def __init__(self, instance_type: "Entity", implements: "Implement") -> None: self.implements = implements self.location = self.implements.get_location() - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: # Only track promises for implementations when they get emitted, because of limitation of current static normalization # order: implementation blocks have not normalized at this point, so with the current mechanism we can't fetch eager # promises yet. Normalization order can not just be reversed because implementation bodies might contain constructor @@ -272,7 +283,7 @@ def __init__(self, condition: ExpressionStatement, if_branch: BasicBlock, else_b def __repr__(self) -> str: return "If" - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: self.condition.normalize() self.if_branch.normalize() self.else_branch.normalize() @@ -329,10 +340,11 @@ def __init__( self.anchors.extend(if_expression.get_anchors()) self.anchors.extend(else_expression.get_anchors()) - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: self.condition.normalize() - self.if_expression.normalize() - self.else_expression.normalize() + # pass on lhs_attribute to branches + self.if_expression.normalize(lhs_attribute=lhs_attribute) + self.else_expression.normalize(lhs_attribute=lhs_attribute) self._own_eager_promises = [ *self.if_expression.get_all_eager_promises(), *self.else_expression.get_all_eager_promises(), @@ -429,13 +441,13 @@ class IndexAttributeMissingInConstructorException(TypingException): Raised when an index attribute was not set in the constructor call for an entity. """ - def __init__(self, stmt: Optional[Locatable], entity: "Entity", unset_attributes: List[str]): + def __init__(self, stmt: Optional[Locatable], entity: "Entity", unset_attributes: abc.Sequence[str]): if not unset_attributes: raise Exception("Argument `unset_attributes` should contain at least one element") error_message = self._get_error_message(entity, unset_attributes) super(IndexAttributeMissingInConstructorException, self).__init__(stmt, error_message) - def _get_error_message(self, entity: "Entity", unset_attributes: List[str]) -> str: + def _get_error_message(self, entity: "Entity", unset_attributes: abc.Sequence[str]) -> str: exc_message = "Invalid Constructor call:" for attribute_name in unset_attributes: attribute: Optional[Attribute] = entity.get_attribute(attribute_name) @@ -462,7 +474,9 @@ class Constructor(ExpressionStatement): "__wrapped_kwarg_attributes", "location", "type", - "required_kwargs", + "_self_ref", + "_lhs_attribute", + "_required_dynamic_args", "_direct_attributes", "_indirect_attributes", ) @@ -485,7 +499,11 @@ def __init__( for a in attributes: self.add_attribute(a[0], a[1]) self.type: Optional["EntityLike"] = None - self.required_kwargs: List[str] = [] + self._self_ref: "Reference" = Reference( + LocatableString(str(uuid.uuid4()), Range("__internal__", 1, 1, 1, 1), -1, self.namespace) + ) + self._lhs_attribute: Optional[AttributeAssignmentLHS] = None + self._required_dynamic_args: list[str] = [] # index attributes required from kwargs or lhs_attribute self._direct_attributes = {} # type: Dict[str,ExpressionStatement] self._indirect_attributes = {} # type: Dict[str,ExpressionStatement] @@ -501,17 +519,19 @@ def pretty_print(self) -> str: ), ) - def normalize(self) -> None: - mytype: "EntityLike" = self.namespace.get_type(self.class_type) - self.type = mytype - + def _normalize_rhs(self, index_attributes: abc.Set[str]) -> None: for (k, v) in self.__attributes.items(): - v.normalize() - + # don't notify the rhs for index attributes because it won't be able to resolve the reference + # (index attributes need to be resolved before the instance can be constructed) + v.normalize(lhs_attribute=AttributeAssignmentLHS(self._self_ref, k) if k not in index_attributes else None) for wrapped_kwargs in self.wrapped_kwargs: wrapped_kwargs.normalize() - inindex = set() + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: + mytype: "EntityLike" = self.namespace.get_type(self.class_type) + self.type = mytype + + inindex: abc.MutableSet[str] = set() all_attributes = dict(self.type.get_default_values()) all_attributes.update(self.__attributes) @@ -521,11 +541,21 @@ def normalize(self) -> None: for index in self.type.get_entity().get_indices(): for attr in index: if attr not in all_attributes: - self.required_kwargs.append(attr) + self._required_dynamic_args.append(attr) continue inindex.add(attr) - if self.required_kwargs and not self.wrapped_kwargs: - raise IndexAttributeMissingInConstructorException(self, self.type.get_entity(), self.required_kwargs) + + if self._required_dynamic_args: + # Limit dynamic compile-time overhead: ignore lhs if this constructor doesn't need it for instantiation. + # Concretely, only store it if not all index attributes are explicitly set in the constructor. + self._lhs_attribute = lhs_attribute + # raise an exception if there are more required dynamic arguments than could be provided by the kwargs and + # lhs attribute. If this passes but the kwargs and/or lhs attribute don't in fact provide the required arguments, + # an exception is raised during execution. + if not self.wrapped_kwargs and (self._lhs_attribute is None or len(self._required_dynamic_args) > 1): + raise IndexAttributeMissingInConstructorException(self, self.type.get_entity(), self._required_dynamic_args) + + self._normalize_rhs(inindex) for (k, v) in all_attributes.items(): attribute = self.type.get_entity().get_attribute(k) @@ -563,6 +593,11 @@ def requires_emit(self, resolver: Resolver, queue: QueueScheduler) -> Dict[objec direct_requires.update( {rk: rv for kwargs in self.__wrapped_kwarg_attributes for (rk, rv) in kwargs.requires_emit(resolver, queue).items()} ) + if self._lhs_attribute is not None: + direct_requires.update( + # if lhs_attribute is set, it is likely required for construction (only exception is if it is in kwargs) + self._lhs_attribute.instance.requires_emit(resolver, queue) + ) LOGGER.log( LOG_LEVEL_TRACE, "emitting constructor for %s at %s with %s", self.class_type, self.location, direct_requires ) @@ -577,44 +612,87 @@ def requires_emit(self, resolver: Resolver, queue: QueueScheduler) -> Dict[objec return requires - def execute(self, requires: Dict[object, object], resolver: Resolver, queue: QueueScheduler): + def _collect_required_dynamic_arguments( + self, requires: Dict[object, object], resolver: Resolver, queue: QueueScheduler + ) -> abc.Mapping[str, object]: """ - Evaluate this statement. + Part of the execute flow: returns values for kwargs and the inverse relation derived from the lhs for which this + constructor is the rhs, if appliccable. """ - LOGGER.log(LOG_LEVEL_TRACE, "executing constructor for %s at %s", self.class_type, self.location) - super().execute(requires, resolver, queue) - - # the type to construct type_class = self.type.get_entity() # kwargs - kwarg_attrs: Dict[str, object] = {} + kwarg_attrs: dict[str, object] = {} for kwargs in self.wrapped_kwargs: for (k, v) in kwargs.execute(requires, resolver, queue): if k in self.attributes or k in kwarg_attrs: raise RuntimeException( self, "The attribute %s is set twice in the constructor call of %s." % (k, self.class_type) ) - attribute = self.type.get_entity().get_attribute(k) + attribute = type_class.get_attribute(k) if attribute is None: raise TypingException(self, "no attribute %s on type %s" % (k, self.type.get_full_name())) kwarg_attrs[k] = v - missing_attrs: List[str] = [attr for attr in self.required_kwargs if attr not in kwarg_attrs] + lhs_inverse_assignment: Optional[tuple[str, object]] = None + # add inverse relation if it is part of an index + if self._lhs_attribute is not None: + lhs_instance: object = self._lhs_attribute.instance.execute(requires, resolver, queue) + if not isinstance(lhs_instance, Instance): + # bug in internal implementation + raise Exception("Invalid state: received lhs_attribute that is not an instance") + lhs_attribute: Optional[Attribute] = lhs_instance.get_type().get_attribute(self._lhs_attribute.attribute) + if not isinstance(lhs_attribute, RelationAttribute): + # bug in the model + raise RuntimeException( + self, + ( + f"Attempting to assign constructor of type {type_class} to attribute that is not a relation attribute:" + f" {lhs_attribute} on {lhs_instance}" + ), + ) + inverse: Optional[RelationAttribute] = lhs_attribute.end + if ( + inverse is not None + and inverse.name not in self._direct_attributes + # in case of a double set, prefer kwargs: double set will be raised when the bidirictional relation is set by + # the LHS + and inverse.name not in kwarg_attrs + and inverse.name in chain.from_iterable(type_class.get_indices()) + and (inverse.entity == type_class or type_class.is_parent(inverse.entity)) + ): + lhs_inverse_assignment = (inverse.name, lhs_instance) + + late_args = {**dict([lhs_inverse_assignment] if lhs_inverse_assignment is not None else []), **kwarg_attrs} + missing_attrs: abc.Sequence[str] = [attr for attr in self._required_dynamic_args if attr not in late_args] if missing_attrs: raise IndexAttributeMissingInConstructorException(self, type_class, missing_attrs) + return late_args + + def execute(self, requires: Dict[object, object], resolver: Resolver, queue: QueueScheduler): + """ + Evaluate this statement. + """ + LOGGER.log(LOG_LEVEL_TRACE, "executing constructor for %s at %s", self.class_type, self.location) + super().execute(requires, resolver, queue) + + # the type to construct + type_class = self.type.get_entity() + + # kwargs and implicit inverse from lhs + late_args: abc.Mapping[str, object] = self._collect_required_dynamic_arguments(requires, resolver, queue) # Schedule all direct attributes for direct execution. The kwarg keys and the direct_attributes keys are disjoint # because a RuntimeException is raised above when they are not. direct_attributes: Dict[str, object] = { k: v.execute(requires, resolver, queue) for (k, v) in self._direct_attributes.items() } - direct_attributes.update(kwarg_attrs) + direct_attributes.update(late_args) # Override defaults with kwargs. The kwarg keys and the indirect_attributes keys are disjoint because a RuntimeException # is raised above when they are not. indirect_attributes: Dict[str, ExpressionStatement] = { - k: v for k, v in self._indirect_attributes.items() if k not in kwarg_attrs + k: v for k, v in self._indirect_attributes.items() if k not in late_args } # check if the instance already exists in the index (if there is one) @@ -631,6 +709,7 @@ def execute(self, requires: Dict[object, object], resolver: Resolver, queue: Que raise DuplicateException(self, obj, "Type found in index is not an exact match") instances.append(obj) + object_instance: Instance graph: Optional[DataflowGraph] = resolver.dataflow_graph if len(instances) > 0: if graph is not None: @@ -657,6 +736,10 @@ def execute(self, requires: Dict[object, object], resolver: Resolver, queue: Que ) # deferred execution for indirect attributes + # inject implicit reference to this instance so attributes can resolve the lhs_attribute we promised in _normalize_rhs + self_var: ResultVariable[Instance] = ResultVariable() + self_var.set_value(object_instance, self.location) + self_resolver: VariableResolver = VariableResolver(resolver, self._self_ref.name, self_var) for attributename, valueexpression in indirect_attributes.items(): var = object_instance.get_attribute(attributename) if var.is_multi(): @@ -664,11 +747,11 @@ def execute(self, requires: Dict[object, object], resolver: Resolver, queue: Que # to preserve order on lists used in attributes # while allowing gradual execution on relations reqs = valueexpression.requires_emit_gradual( - resolver, queue, GradualSetAttributeHelper(self, object_instance, attributename, var) + self_resolver, queue, GradualSetAttributeHelper(self, object_instance, attributename, var) ) else: - reqs = valueexpression.requires_emit(resolver, queue) - SetAttributeHelper(queue, resolver, var, reqs, valueexpression, self, object_instance, attributename) + reqs = valueexpression.requires_emit(self_resolver, queue) + SetAttributeHelper(queue, self_resolver, var, reqs, valueexpression, self, object_instance, attributename) # generate an implementation for stmt in type_class.get_sub_constructor(): @@ -745,7 +828,7 @@ def __init__(self, dictionary: ExpressionStatement) -> None: def __repr__(self) -> str: return "**%s" % repr(self.dictionary) - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: self.dictionary.normalize() def get_all_eager_promises(self) -> Iterator["StaticEagerPromise"]: diff --git a/src/inmanta/ast/variables.py b/src/inmanta/ast/variables.py index d08da3a425..46e8c43872 100644 --- a/src/inmanta/ast/variables.py +++ b/src/inmanta/ast/variables.py @@ -23,6 +23,7 @@ from inmanta.ast import LocatableString, Location, NotFoundException, OptionalValueException, Range, RuntimeException from inmanta.ast.statements import ( AssignStatement, + AttributeAssignmentLHS, ExpressionStatement, RawResumer, Statement, @@ -66,7 +67,7 @@ def __init__(self, name: LocatableString) -> None: self.name = str(name) self.full_name = str(name) - def normalize(self) -> None: + def normalize(self, *, lhs_attribute: Optional[AttributeAssignmentLHS] = None) -> None: pass def requires(self) -> List[str]: diff --git a/src/inmanta/execute/runtime.py b/src/inmanta/execute/runtime.py index 2cefbea115..543db6aba1 100644 --- a/src/inmanta/execute/runtime.py +++ b/src/inmanta/execute/runtime.py @@ -1079,6 +1079,34 @@ def get_dataflow_node(self, name: str) -> "dataflow.AssignableNodeReference": return root_graph.get_own_variable(name) +class VariableResolver(Resolver): + """ + Resolver that resolves a single variable to a value, and delegates the rest to its parent resolver. + """ + + __slots__ = ( + "parent", + "name", + "variable", + ) + + def __init__(self, parent: Resolver, name: str, variable: ResultVariable[T]) -> None: + self.parent: Resolver = parent + self.name: str = name + self.variable: ResultVariable[T] = variable + self.dataflow_graph = ( + DataflowGraph(self, parent=self.parent.dataflow_graph) if self.parent.dataflow_graph is not None else None + ) + + def lookup(self, name: str, root: Optional[Namespace] = None) -> Typeorvalue: + if root is None and name == self.name: + return self.variable + return self.parent.lookup(name, root) + + def get_root_resolver(self) -> "Resolver": + return self.parent.get_root_resolver() + + class NamespaceResolver(Resolver): __slots__ = ("parent", "root") diff --git a/src/inmanta/plugins.py b/src/inmanta/plugins.py index 31efc5cbf9..9ce1e2e8b4 100644 --- a/src/inmanta/plugins.py +++ b/src/inmanta/plugins.py @@ -28,14 +28,14 @@ from inmanta.ast.type import NamedType from inmanta.config import Config from inmanta.execute.proxy import DynamicProxy -from inmanta.execute.runtime import ExecutionUnit, QueueScheduler, Resolver, ResultVariable +from inmanta.execute.runtime import QueueScheduler, Resolver, ResultVariable from inmanta.execute.util import Unknown from inmanta.stable_api import stable_api T = TypeVar("T") if TYPE_CHECKING: - from inmanta.ast.statements import DynamicStatement, ExpressionStatement + from inmanta.ast.statements import DynamicStatement from inmanta.ast.statements.call import FunctionCall from inmanta.compiler import Compiler @@ -65,15 +65,6 @@ def __init__( self.result = result self.compiler = queue.get_compiler() - def emit_expression(self, stmt: "ExpressionStatement") -> None: - """ - Add a new statement - """ - self.owner.copy_location(stmt) - stmt.normalize(self.resolver) - reqs = stmt.requires_emit(self.resolver, self.queue) - ExecutionUnit(self.queue, self.resolver, self.result, reqs, stmt, provides=False) - def get_resolver(self) -> Resolver: return self.resolver diff --git a/tests/compiler/test_relations_inverse.py b/tests/compiler/test_relations_inverse.py new file mode 100644 index 0000000000..82db7e510c --- /dev/null +++ b/tests/compiler/test_relations_inverse.py @@ -0,0 +1,360 @@ +""" + Copyright 2022 Inmanta + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Contact: code@inmanta.com +""" +import contextlib +import textwrap + +import pytest + +from inmanta import compiler +from inmanta.ast.statements.generator import IndexAttributeMissingInConstructorException + + +def test_relations_implicit_inverse_simple(snippetcompiler) -> None: + """ + Verify that the basics of implicit inverse relations on index attributes work: if an index attribute is missing in a + constructor call attempt to derive it from the left hand side. + """ + snippetcompiler.setup_for_snippet( + """ + entity A: end + entity B: end + implement A using std::none + implement B using std::none + + A.b [0:1] -- B.a [1] + + index B(a) + + # nested constructors + a1 = A(b=B()) + # attribute assignment + a2 = A() + a2.b = B() + + assert = true + assert = a1.b.a == a1 + assert = a2.b.a == a2 + """ + ) + compiler.do_compile() + + +def test_relations_implicit_inverse_composite_index(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes work for an index consisting of multiple fields. + """ + snippetcompiler.setup_for_snippet( + """ + entity A: end + entity B: + int id + end + implement A using std::none + implement B using std::none + + A.b [0:1] -- B.a [1] + + index B(id, a) + + # nested constructors + a1 = A(b=B(id=0)) + # attribute assignment + a2 = A() + a2.b = B(id=0) + + assert = true + assert = a1.b.a == a1 + assert = a2.b.a == a2 + """ + ) + compiler.do_compile() + + +def test_relations_implicit_inverse_composite_rhs(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes work if the constructor appears as an element in a composite + statement, e.g. a list or a conditional expression. + """ + snippetcompiler.setup_for_snippet( + """ + entity A: end + entity B: + int id = 0 + end + implement A using std::none + implement B using std::none + + A.b [0:] -- B.a [1] + + # not very realistic model, but still a good case to check + index B(id, a) + + # nested constructors + a1 = A(b=[B(id=0), true ? B(id=1) : B(id=2)]) + # attribute assignment + a2 = A() + a2.b = false ? [B(id=10), B(id=20)] : [B(id=30), B(id=40), B(id=50)] + + assert = true + assert = a1.b == [B[id=0, a=a1], B[id=1, a=a1]] + assert = a2.b == [B[id=30, a=a2], B[id=40, a=a2], B[id=50, a=a2]] + """ + ) + compiler.do_compile() + + +@pytest.mark.parametrize("double_index", (True, False)) +def test_relations_implicit_inverse_left_index(snippetcompiler, double_index: bool) -> None: + """ + Verify that the implementation for implicit inverse relations on index attributes does not choke on an index on the left + hand side: a naive implementation might have this behavior. + + :param double_index: Iff True, include an index on the right side as well. This is expected to fail with a meaningful error + message. + """ + snippetcompiler.setup_for_snippet( + """ + entity A: end + entity B: end + implement A using std::none + implement B using std::none + + A.b [1] -- B.a [1] + + index A(b) + %s + + A(b=B()) + """ + % ("index B(a)" if double_index else "") + ) + with ( + pytest.raises( + IndexAttributeMissingInConstructorException, + match="Missing relation 'a'. The relation __config__::B.a is part of an index.", + ) + if double_index + else contextlib.nullcontext() + ): + compiler.do_compile() + + +def test_relation_implicit_inverse_deeply_nested_constructors(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes for deeply nested constructors work as expected. + """ + snippetcompiler.setup_for_snippet( + """ + entity A: end + entity B: end + entity C: end + entity D: end + implement A using std::none + implement B using std::none + implement C using std::none + implement D using std::none + + A.b [1] -- B.a [1] + B.c [1] -- C.b [1] + C.d [1] -- D.c [1] + + index B(a) + index C(b) + index D(c) + + a = A(b=B(c=C(d=D()))) + + assert = true + assert = a.b.a == a + assert = a.b.c.b == a.b + assert = a.b.c.d.c == a.b.c + """ + ) + compiler.do_compile() + + +def test_relation_implicit_inverse_nested_constructors_same_entity(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes for deeply nested constructors for the same entity type work as + expected and that inverse relations are set to the immediate parent in the constructor tree. + """ + snippetcompiler.setup_for_snippet( + """ + entity LHS: + end + entity RHS extends LHS: + end + + LHS.right [0:1] -- RHS.left [1] + index RHS(left) + + implement LHS using std::none + implement RHS using std::none + + x1 = LHS( + right=RHS( # x2: inverse relation x2.left should be set to x1 + right=RHS( # x3: inverse relation x3.left should be set to x2 + right=RHS() # x4: inverse relation x4.left should be set to x3 + ) + ) + ) + + assert = true + assert = x1.right.left == x1 + assert = x1.right.right.left == x1.right + assert = x1.right.right.right.left == x1.right.right + assert = x1 != x1.right + assert = x1.right != x1.right.right + """ + ) + compiler.do_compile() + + +def test_relation_implicit_inverse_kwargs_conflict(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes don't hide conflicts with explicit assignments through kwargs. + """ + snippetcompiler.setup_for_error( + """ + entity A: end + entity B: end + implement A using std::none + implement B using std::none + + A.b [0:1] -- B.a [1] + + index B(a) + + # nested constructors + b_kwargs = {"a": A()} + a1 = A(b=B(**b_kwargs)) + + assert = true + assert = a1.b.a == a1 + """, + textwrap.dedent( + """ + Could not set attribute `a` on instance `__config__::B (instantiated at {dir}/main.cf:13)` (reported in __config__::B (instantiated at {dir}/main.cf:13) ({dir}/main.cf:13)) + caused by: + value set twice: + \told value: __config__::A (instantiated at {dir}/main.cf:12) + \t\tset at {dir}/main.cf:13 + \tnew value: __config__::A (instantiated at {dir}/main.cf:13) + \t\tset at {dir}/main.cf:13 + (reported in Construct(A) ({dir}/main.cf:13)) + """.lstrip( # noqa: E501 + "\n" + ).rstrip() + ), + ) + + +def test_relation_implicit_inverse_on_plain_attribute(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes don't hide errors due to relation assignment to a plain attribute + """ + snippetcompiler.setup_for_error( + """ + entity A: + int b + end + entity B: end + implement A using std::none + implement B using std::none + + B.a [1] -- A + index B(a) + + A(b=B()) + """, + textwrap.dedent( + """ + Could not set attribute `b` on instance `__config__::A (instantiated at {dir}/main.cf:12)` (reported in Construct(A) ({dir}/main.cf:12)) + caused by: + Attempting to assign constructor of type __config__::B to attribute that is not a relation attribute: b on __config__::A (instantiated at {dir}/main.cf:12) (reported in Construct(B) ({dir}/main.cf:12)) + """.lstrip( # noqa: E501 + "\n" + ).rstrip() + ), + ) + + +def test_relation_implicit_inverse_on_different_entity_type(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes don't hide errors due to relation assignment to a wrong + entity type. + """ + snippetcompiler.setup_for_error( + """ + entity A: end + entity B: end + entity C: end + implement A using std::none + implement B using std::none + implement C using std::none + + A.b [0:1] -- C.a [1] + B.a [1] -- A + index B(a) + + A(b=B()) + """, + textwrap.dedent( + """ + Could not set attribute `b` on instance `__config__::A (instantiated at {dir}/main.cf:13)` (reported in Construct(A) ({dir}/main.cf:13)) + caused by: + Invalid Constructor call: + \t* Missing relation 'a'. The relation __config__::B.a is part of an index. (reported in Construct(B) ({dir}/main.cf:13)) + """.lstrip( # noqa: E501 + "\n" + ).rstrip() + ), + ) + + +def test_relation_implicit_inverse_inheritance(snippetcompiler) -> None: + """ + Verify that implicit inverse relations on index attributes work as expected when combined with inheritance: relations and + indexes defined on parent entities should allow implicit inverses on their children. + """ + snippetcompiler.setup_for_snippet( + """ + entity AABC: end + entity BABC: end + entity ChildA extends AABC: end + entity ChildB extends BABC: end + implement ChildA using std::none + implement ChildB using std::none + + # relation and index on ABC + AABC.b [0:1] -- BABC.a [1] + + index BABC(a) + + # nested constructors + a1 = ChildA(b=ChildB()) + # attribute assignment + a2 = ChildA() + a2.b = ChildB() + + assert = true + assert = a1.b.a == a1 + assert = a2.b.a == a2 + """ + ) + compiler.do_compile()