Repository: reagento/adaptix (language: python)

Issue:
Add ``mediator.cached_call`` to other providers



Discussion / hints:

Fix patch:
diff --git a/src/adaptix/_internal/datastructures.py b/src/adaptix/_internal/datastructures.py
index a4ea8ff5..054b100a 100644
--- a/src/adaptix/_internal/datastructures.py
+++ b/src/adaptix/_internal/datastructures.py
@@ -23,6 +23,7 @@
 )
 
 from .common import VarTuple
+from .utils import MappingHashWrapper
 
 K = TypeVar("K", bound=Hashable)
 V = TypeVar("V")
@@ -114,6 +115,9 @@ def __eq__(self, other):
             return self._mapping == other._mapping
         return NotImplemented
 
+    def __hash__(self):
+        return hash(MappingHashWrapper(self._mapping))
+
 
 # It's not a KeysView because __iter__ of KeysView must returns an Iterator[K_co]
 # but there is no inverse of Type[]
diff --git a/src/adaptix/_internal/morphing/concrete_provider.py b/src/adaptix/_internal/morphing/concrete_provider.py
index 2638b6cb..2f605c38 100644
--- a/src/adaptix/_internal/morphing/concrete_provider.py
+++ b/src/adaptix/_internal/morphing/concrete_provider.py
@@ -39,6 +39,9 @@ def __repr__(self):
         return f"{type(self)}(cls={self._cls})"
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
+        return mediator.cached_call(self._make_loader)
+
+    def _make_loader(self):
         raw_loader = self._cls.fromisoformat
 
         def isoformat_loader(data):
@@ -48,10 +51,12 @@ def isoformat_loader(data):
                 raise TypeLoadError(str, data)
             except ValueError:
                 raise ValueLoadError("Invalid isoformat string", data)
-
         return isoformat_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         return self._cls.isoformat
 
     def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema:
@@ -67,6 +72,9 @@ def __repr__(self):
         return f"{type(self)}(fmt={self._fmt})"
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
+        return mediator.cached_call(self._make_loader)
+
+    def _make_loader(self):
         fmt = self._fmt
 
         def datetime_format_loader(data):
@@ -80,6 +88,9 @@ def datetime_format_loader(data):
         return datetime_format_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         fmt = self._fmt
 
         def datetime_format_dumper(data: datetime):
@@ -97,6 +108,9 @@ def __init__(self, tz: Optional[timezone]):
         self._tz = tz
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
+        return mediator.cached_call(self._make_loader)
+
+    def _make_loader(self):
         tz = self._tz
 
         def datetime_timestamp_loader(data):
@@ -115,9 +129,11 @@ def datetime_timestamp_loader(data):
         return datetime_timestamp_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         def datetime_timestamp_dumper(data: datetime):
             return data.timestamp()
-
         return datetime_timestamp_dumper
 
     def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema:
@@ -138,6 +154,9 @@ def _is_pydatetime(self) -> bool:
         return False
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
+        return mediator.cached_call(self._make_loader)
+
+    def _make_loader(self):
         def date_timestamp_loader(data):
             try:
                 # Pure-Python implementation and C-extension implementation
@@ -171,6 +190,9 @@ def pydate_timestamp_loader(data):
         return pydate_timestamp_loader if self._is_pydatetime() else date_timestamp_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         def date_timestamp_dumper(data: date):
             dt = datetime(
                 year=data.year,
@@ -179,7 +201,6 @@ def date_timestamp_dumper(data: date):
                 tzinfo=timezone.utc,
             )
             return dt.timestamp()
-
         return date_timestamp_dumper
 
     def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema:
@@ -191,6 +212,9 @@ class SecondsTimedeltaProvider(MorphingProvider):
     _OK_TYPES = (int, float, Decimal)
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
+        return mediator.cached_call(self._make_loader)
+
+    def _make_loader(self):
         ok_types = self._OK_TYPES
 
         def timedelta_loader(data):
@@ -201,6 +225,9 @@ def timedelta_loader(data):
         return timedelta_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         return timedelta.total_seconds
 
     def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema:
@@ -227,12 +254,13 @@ def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest)
 
 class _Base64DumperMixin(DumperProvider):
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         def bytes_base64_dumper(data):
             return b2a_base64(data, newline=False).decode("ascii")
-
         return bytes_base64_dumper
 
-
 class _Base64JSONSchemaMixin(JSONSchemaProvider):
     def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema:
         return JSONSchema(type=JSONSchemaType.STRING, content_encoding="base64")
@@ -244,6 +272,9 @@ def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest)
 @for_predicate(bytes)
 class BytesBase64Provider(_Base64DumperMixin, _Base64JSONSchemaMixin, MorphingProvider):
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
+        return mediator.cached_call(self._make_loader)
+
+    def _make_loader(self):
         def bytes_base64_loader(data):
             try:
                 encoded = data.encode("ascii")
@@ -257,7 +288,6 @@ def bytes_base64_loader(data):
                 return a2b_base64(encoded)
             except binascii.Error as e:
                 raise ValueLoadError(str(e), data)
-
         return bytes_base64_loader
 
 
@@ -266,29 +296,36 @@ class BytesIOBase64Provider(_Base64JSONSchemaMixin, MorphingProvider):
     _BYTES_PROVIDER = BytesBase64Provider()
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
-        bytes_base64_loader = self._BYTES_PROVIDER.provide_loader(mediator, request)
+        return mediator.cached_call(
+            self._make_loader,
+            loader=self._BYTES_PROVIDER.provide_loader(mediator, request),
+        )
 
+    def _make_loader(self, loader: Loader):
         def bytes_io_base64_loader(data):
-            return BytesIO(bytes_base64_loader(data))
-
+            return BytesIO(loader(data))
         return bytes_io_base64_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         def bytes_io_base64_dumper(data: BytesIO):
             return b2a_base64(data.getvalue(), newline=False).decode("ascii")
-
         return bytes_io_base64_dumper
 
 
 @for_predicate(typing.IO[bytes])
 class IOBytesBase64Provider(BytesIOBase64Provider, _Base64JSONSchemaMixin, MorphingProvider):
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
+        return mediator.cached_call(self._make_dumper)
+
+    def _make_dumper(self):
         def io_bytes_base64_dumper(data: typing.IO[bytes]):
             if data.seekable():
                 data.seek(0)
 
             return b2a_base64(data.read(), newline=False).decode("ascii")
-
         return io_bytes_base64_dumper
 
 
@@ -302,9 +339,14 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
             replace(request, loc_stack=request.loc_stack.replace_last_type(bytes)),
         )
 
-        def bytearray_base64_loader(data):
-            return bytearray(bytes_loader(data))
+        return mediator.cached_call(
+            self._make_loader,
+            loader=bytes_loader,
+        )
 
+    def _make_loader(self, loader: Loader):
+        def bytearray_base64_loader(data):
+            return bytearray(loader(data))
         return bytearray_base64_loader
 
 
@@ -318,6 +360,9 @@ def __init__(self, flags: re.RegexFlag = re.RegexFlag(0)):
         self.flags = flags
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
+        return mediator.cached_call(self._make_loader)
+
+    def _make_loader(self):
         flags = self.flags
         re_compile = re.compile
 
@@ -360,6 +405,12 @@ def __init__(
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
         strict_coercion = mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack))
+        return mediator.cached_call(
+            self._make_loader,
+            strict_coercion=strict_coercion,
+        )
+
+    def _make_loader(self, *, strict_coercion: bool):
         return self._strict_coercion_loader if strict_coercion else self._lax_coercion_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
diff --git a/src/adaptix/_internal/morphing/constant_length_tuple_provider.py b/src/adaptix/_internal/morphing/constant_length_tuple_provider.py
index 31b392ad..9ad20aae 100644
--- a/src/adaptix/_internal/morphing/constant_length_tuple_provider.py
+++ b/src/adaptix/_internal/morphing/constant_length_tuple_provider.py
@@ -48,7 +48,12 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
         )
         strict_coercion = mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack))
         debug_trail = mediator.mandatory_provide(DebugTrailRequest(loc_stack=request.loc_stack))
-        return self._make_loader(tuple(loaders), strict_coercion=strict_coercion, debug_trail=debug_trail)
+        return mediator.cached_call(
+            self._make_loader,
+            loaders=tuple(loaders),
+            strict_coercion=strict_coercion,
+            debug_trail=debug_trail,
+        )
 
     def _make_loader(self, loaders: Collection[Loader], *, strict_coercion: bool, debug_trail: DebugTrail):
         if debug_trail == DebugTrail.DISABLE:
@@ -225,7 +230,11 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
             lambda: "Cannot create dumper for tuple. Dumpers for some elements cannot be created",
         )
         debug_trail = mediator.mandatory_provide(DebugTrailRequest(loc_stack=request.loc_stack))
-        return self._make_dumper(tuple(dumpers), debug_trail)
+        return mediator.cached_call(
+            self._make_dumper,
+            dumpers=tuple(dumpers),
+            debug_trail=debug_trail,
+        )
 
     def _make_dumper(self, dumpers: Collection[Dumper], debug_trail: DebugTrail):
         if debug_trail == DebugTrail.DISABLE:
diff --git a/src/adaptix/_internal/morphing/dict_provider.py b/src/adaptix/_internal/morphing/dict_provider.py
index cc585c9c..db11bf9d 100644
--- a/src/adaptix/_internal/morphing/dict_provider.py
+++ b/src/adaptix/_internal/morphing/dict_provider.py
@@ -44,7 +44,8 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
         debug_trail = mediator.mandatory_provide(
             DebugTrailRequest(loc_stack=request.loc_stack),
         )
-        return self._make_loader(
+        return mediator.cached_call(
+            self._make_loader,
             key_loader=key_loader,
             value_loader=value_loader,
             debug_trail=debug_trail,
@@ -159,7 +160,8 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
         debug_trail = mediator.mandatory_provide(
             DebugTrailRequest(loc_stack=request.loc_stack),
         )
-        return self._make_dumper(
+        return mediator.cached_call(
+            self._make_dumper,
             key_dumper=key_dumper,
             value_dumper=value_dumper,
             debug_trail=debug_trail,
@@ -261,18 +263,24 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
             mediator,
             replace(request, loc_stack=request.loc_stack.replace_last_type(dict_type_hint)),
         )
+
+        return mediator.cached_call(
+            self._make_loader,
+            loader=dict_loader,
+        )
+
+    def _make_loader(self, loader: Loader):
         default_factory = self.default_factory
 
         def defaultdict_loader(data):
-            return defaultdict(default_factory, dict_loader(data))
+            return defaultdict(default_factory, loader(data))
 
         return defaultdict_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
         key, value = self._extract_key_value(request)
         dict_type_hint = Dict[key.source, value.source]  # type: ignore[misc, name-defined]
-
         return self._DICT_PROVIDER.provide_dumper(
             mediator,
-            replace(request, loc_stack=request.loc_stack.replace_last_type(dict_type_hint)),
+            request=replace(request, loc_stack=request.loc_stack.replace_last_type(dict_type_hint)),
         )
diff --git a/src/adaptix/_internal/morphing/enum_provider.py b/src/adaptix/_internal/morphing/enum_provider.py
index 8d859b29..75f3460d 100644
--- a/src/adaptix/_internal/morphing/enum_provider.py
+++ b/src/adaptix/_internal/morphing/enum_provider.py
@@ -108,7 +108,12 @@ def __init__(self, mapping_generator: BaseEnumMappingGenerator):
         self._mapping_generator = mapping_generator
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
-        enum = request.last_loc.type
+        return mediator.cached_call(
+            self._make_loader,
+            enum=request.last_loc.type,
+        )
+
+    def _make_loader(self, enum):
         mapping = self._mapping_generator.generate_for_loading(enum.__members__.values())
         variants = list(mapping.keys())
 
@@ -124,6 +129,13 @@ def enum_loader(data):
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
         enum = request.last_loc.type
+
+        return mediator.cached_call(
+            self._make_dumper,
+            enum=enum,
+        )
+
+    def _make_dumper(self, enum):
         mapping = self._mapping_generator.generate_for_dumping(enum.__members__.values())
 
         def enum_dumper(data: Enum) -> str:
@@ -142,6 +154,13 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
             request.append_loc(TypeHintLoc(type=self._value_type)),
         )
 
+        return mediator.cached_call(
+            self._make_loader,
+            enum=enum,
+            value_loader=value_loader,
+        )
+
+    def _make_loader(self, enum: Enum, value_loader: Loader):
         def enum_loader(data):
             loaded_value = value_loader(data)
             try:
@@ -156,21 +175,30 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
             request.append_loc(TypeHintLoc(type=self._value_type)),
         )
 
+        return mediator.cached_call(
+            self._make_dumper,
+            value_dumper=value_dumper,
+        )
+
+    def _make_dumper(self, value_dumper: Dumper):
         def enum_dumper(data):
             return value_dumper(data.value)
 
         return enum_dumper
 
-
 class EnumExactValueProvider(BaseEnumProvider):
     """This provider represents enum members to the outside world
     by their value without any processing
     """
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
-        enum = request.last_loc.type
-        variants = [case.value for case in enum]
+        return mediator.cached_call(
+            self._make_loader,
+            enum=request.last_loc.type,
+        )
 
+    def _make_loader(self, enum):
+        variants = [case.value for case in enum]
         value_to_member = self._get_exact_value_to_member(enum)
         if value_to_member is None:
             def enum_exact_loader(data):
@@ -195,6 +223,7 @@ def enum_exact_loader_v2m(data):
 
         return enum_exact_loader_v2m
 
+
     def _get_exact_value_to_member(self, enum: Type[Enum]) -> Optional[Mapping[Any, Any]]:
         try:
             value_to_member = {member.value: member for member in enum}
@@ -207,7 +236,13 @@ def _get_exact_value_to_member(self, enum: Type[Enum]) -> Optional[Mapping[Any,
         return value_to_member
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
-        member_to_value = {member: member.value for member in request.last_loc.type}
+        return mediator.cached_call(
+            self._make_dumper,
+            enum=request.last_loc.type,
+        )
+
+    def _make_dumper(self, enum):
+        member_to_value = {member: member.value for member in enum}
 
         def enum_exact_value_dumper(data):
             return member_to_value[data]
@@ -217,7 +252,12 @@ def enum_exact_value_dumper(data):
 
 class FlagByExactValueProvider(BaseFlagProvider):
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
-        enum = request.last_loc.type
+        return mediator.cached_call(
+            self._make_loader,
+            enum=request.last_loc.type,
+        )
+
+    def _make_loader(self, enum):
         flag_mask = reduce(or_, enum.__members__.values()).value
 
         if flag_mask < 0:
@@ -249,11 +289,10 @@ def flag_loader(data):
         return flag_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
-        def flag_exact_value_dumper(data):
-            return data.value
-
         return flag_exact_value_dumper
 
+def flag_exact_value_dumper(data):
+    return data.value
 
 def _extract_non_compound_cases_from_flag(enum: Type[FlagT]) -> Sequence[FlagT]:
     return [case for case in enum.__members__.values() if not math.log2(case.value) % 1]
@@ -282,6 +321,13 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
         enum = request.last_loc.type
 
         strict_coercion = mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack))
+        return mediator.cached_call(
+            self._make_loader,
+            enum=enum,
+            strict_coercion=strict_coercion,
+        )
+
+    def _make_loader(self, enum, *, strict_coercion: bool):
         allow_single_value = self._allow_single_value
         allow_duplicates = self._allow_duplicates
 
@@ -329,8 +375,12 @@ def flag_loader(data) -> Flag:
         return flag_loader
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
-        enum = request.last_loc.type
+        return mediator.cached_call(
+            self._make_dumper,
+            enum=request.last_loc.type,
+        )
 
+    def _make_dumper(self, enum):
         cases = self._get_cases(enum)
         need_to_reverse = self._allow_compound and cases != _extract_non_compound_cases_from_flag(enum)
         if need_to_reverse:
diff --git a/src/adaptix/_internal/morphing/generic_provider.py b/src/adaptix/_internal/morphing/generic_provider.py
index d6206cc7..a1e9ea06 100644
--- a/src/adaptix/_internal/morphing/generic_provider.py
+++ b/src/adaptix/_internal/morphing/generic_provider.py
@@ -3,7 +3,7 @@
 from enum import Enum
 from os import PathLike
 from pathlib import Path
-from typing import Any, Collection, Dict, Iterable, Literal, Optional, Sequence, Set, Type, TypeVar, Union
+from typing import Any, Collection, Iterable, Literal, Mapping, Optional, Sequence, Set, Type, TypeVar, Union
 
 from ..common import Dumper, Loader, TypeHint
 from ..compat import CompatExceptionGroup
@@ -16,6 +16,7 @@
 from ..provider.location import GenericParamLoc, TypeHintLoc
 from ..special_cases_optimization import as_is_stub
 from ..type_tools import BaseNormType, NormTypeAlias, is_new_type, is_subclass_soft, strip_tags
+from ..utils import MappingHashWrapper
 from .load_error import BadVariantLoadError, LoadError, TypeLoadError, UnionLoadError
 from .provider_template import DumperProvider, LoaderProvider
 from .request_cls import DebugTrailRequest, DumperRequest, LoaderRequest, StrictCoercionRequest
@@ -75,10 +76,10 @@ def _get_allowed_values_collection(self, args: Collection) -> Collection:
     def _get_allowed_values_repr(self, args: Collection, mediator: Mediator, loc_stack: LocStack) -> Collection:
         enum_cases = [arg for arg in args if isinstance(arg, Enum)]
         if not enum_cases:
-            return set(args)
+            return frozenset(args)
 
         literal_dumper = self.provide_dumper(mediator, DumperRequest(loc_stack))
-        return {literal_dumper(arg) if isinstance(arg, Enum) else arg for arg in args}
+        return frozenset(literal_dumper(arg) if isinstance(arg, Enum) else arg for arg in args)
 
     def _get_enum_types(self, cases: Collection) -> Collection:
         seen: Set[Type[Enum]] = set()
@@ -104,7 +105,7 @@ def _fetch_enum_loaders(
 
     def _fetch_enum_dumpers(
         self, mediator: Mediator, request: DumperRequest, enum_classes: Iterable[Type[Enum]],
-    ) -> Dict[Type[Enum], Dumper[Enum]]:
+    ) -> Mapping[Type[Enum], Dumper[Enum]]:
         requests = [
             request.append_loc(TypeHintLoc(type=enum_cls))
             for enum_cls in enum_classes
@@ -152,17 +153,31 @@ def wrapped_loader_with_enums(data):
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
         norm = try_normalize_type(request.last_loc.type)
         strict_coercion = mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack))
-
-        enum_cases = [arg for arg in norm.args if isinstance(arg, Enum)]
-        enum_loaders = list(self._fetch_enum_loaders(mediator, request, self._get_enum_types(enum_cases)))
+        enum_cases = tuple(arg for arg in norm.args if isinstance(arg, Enum))
+        enum_loaders = tuple(self._fetch_enum_loaders(mediator, request, self._get_enum_types(enum_cases)))
         allowed_values_repr = self._get_allowed_values_repr(norm.args, mediator, request.loc_stack)
+        return mediator.cached_call(
+            self._make_loader,
+            cases=norm.args,
+            strict_coercion=strict_coercion,
+            enum_loaders=enum_loaders,
+            allowed_values_repr=allowed_values_repr,
+        )
 
+    def _make_loader(
+        self,
+        *,
+        cases: Sequence[Any],
+        strict_coercion: bool,
+        enum_loaders: Sequence[Loader],
+        allowed_values_repr: Collection[str],
+    ) -> Loader:
         if strict_coercion and any(
             isinstance(arg, bool) or _is_exact_zero_or_one(arg)
-            for arg in norm.args
+            for arg in cases
         ):
             allowed_values_with_types = self._get_allowed_values_collection(
-                [(type(el), el) for el in norm.args],
+                [(type(el), el) for el in cases],
             )
 
             # since True == 1 and False == 0
@@ -175,7 +190,7 @@ def literal_loader_sc(data):
                 literal_loader_sc, enum_loaders, allowed_values_with_types,
             )
 
-        allowed_values = self._get_allowed_values_collection(norm.args)
+        allowed_values = self._get_allowed_values_collection(cases)
 
         def literal_loader(data):
             if data in allowed_values:
@@ -192,6 +207,13 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
             return as_is_stub
 
         enum_dumpers = self._fetch_enum_dumpers(mediator, request, self._get_enum_types(enum_cases))
+        return mediator.cached_call(
+            self._make_dumper,
+            enum_dumpers_wrapper=MappingHashWrapper(enum_dumpers),
+        )
+
+    def _make_dumper(self, enum_dumpers_wrapper: MappingHashWrapper[Mapping[Type[Enum], Dumper[Enum]]]):
+        enum_dumpers = enum_dumpers_wrapper.mapping
 
         if len(enum_dumpers) == 1:
             enum_dumper = next(iter(enum_dumpers.values()))
@@ -229,9 +251,9 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
                 lambda x: "Cannot create loader for union. Loaders for some union cases cannot be created",
             )
             if debug_trail in (DebugTrail.ALL, DebugTrail.FIRST):
-                return self._single_optional_dt_loader(norm.source, not_none_loader)
+                return mediator.cached_call(self._single_optional_dt_loader, norm.source, not_none_loader)
             if debug_trail == DebugTrail.DISABLE:
-                return self._single_optional_dt_disable_loader(not_none_loader)
+                return mediator.cached_call(self._single_optional_dt_disable_loader, not_none_loader)
             raise ValueError
 
         loaders = mediator.mandatory_provide_by_iterable(
@@ -247,11 +269,11 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
             lambda: "Cannot create loader for union. Loaders for some union cases cannot be created",
         )
         if debug_trail == DebugTrail.DISABLE:
-            return self._get_loader_dt_disable(tuple(loaders))
+            return mediator.cached_call(self._get_loader_dt_disable, tuple(loaders))
         if debug_trail == DebugTrail.FIRST:
-            return self._get_loader_dt_first(norm.source, tuple(loaders))
+            return mediator.cached_call(self._get_loader_dt_first, norm.source, tuple(loaders))
         if debug_trail == DebugTrail.ALL:
-            return self._get_loader_dt_all(norm.source, tuple(loaders))
+            return mediator.cached_call(self._get_loader_dt_all, norm.source, tuple(loaders))
         raise ValueError
 
     def _single_optional_dt_disable_loader(self, loader: Loader) -> Loader:
@@ -342,7 +364,7 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
             )
             if not_none_dumper == as_is_stub:
                 return as_is_stub
-            return self._get_single_optional_dumper(not_none_dumper)
+            return mediator.cached_call(self._get_single_optional_dumper, not_none_dumper)
 
         forbidden_origins = [
             case.source
@@ -372,6 +394,9 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
         if all(dumper == as_is_stub for dumper in dumpers):
             return as_is_stub
 
+        return mediator.cached_call(self._make_dumper, norm, tuple(dumpers))
+
+    def _make_dumper(self, norm: BaseNormType, dumpers: Iterable[Dumper]) -> Dumper:
         dumper_type_dispatcher = ClassDispatcher(
             {type(None) if case.origin is None else case.origin: dumper for case, dumper in zip(norm.args, dumpers)},
         )
diff --git a/src/adaptix/_internal/morphing/iterable_provider.py b/src/adaptix/_internal/morphing/iterable_provider.py
index fecba340..922deec7 100644
--- a/src/adaptix/_internal/morphing/iterable_provider.py
+++ b/src/adaptix/_internal/morphing/iterable_provider.py
@@ -73,7 +73,8 @@ def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
         )
         strict_coercion = mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack))
         debug_trail = mediator.mandatory_provide(DebugTrailRequest(loc_stack=request.loc_stack))
-        return self._make_loader(
+        return mediator.cached_call(
+            self._make_loader,
             origin=norm.origin,
             iter_factory=iter_factory,
             arg_loader=arg_loader,
@@ -206,7 +207,8 @@ def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
             lambda x: "Cannot create dumper for iterable. Dumper for element cannot be created",
         )
         debug_trail = mediator.mandatory_provide(DebugTrailRequest(loc_stack=request.loc_stack))
-        return self._make_dumper(
+        return mediator.cached_call(
+            self._make_dumper,
             origin=norm.origin,
             iter_factory=iter_factory,
             arg_dumper=arg_dumper,
diff --git a/src/adaptix/_internal/morphing/model/crown_definitions.py b/src/adaptix/_internal/morphing/model/crown_definitions.py
index 9a027a09..88fa68db 100644
--- a/src/adaptix/_internal/morphing/model/crown_definitions.py
+++ b/src/adaptix/_internal/morphing/model/crown_definitions.py
@@ -4,7 +4,7 @@
 from ...common import VarTuple
 from ...model_tools.definitions import BaseShape, DefaultFactory, DefaultValue, InputShape, OutputShape
 from ...provider.located_request import LocatedRequest
-from ...utils import SingletonMeta
+from ...utils import MappingHashWrapper, SingletonMeta
 
 T = TypeVar("T")
 
@@ -35,22 +35,22 @@ class ExtraCollect(metaclass=SingletonMeta):
 # NoneCrown-s represents an element that does not map to any field
 
 
-@dataclass
+@dataclass(frozen=True)
 class BaseDictCrown(Generic[T]):
     map: Mapping[str, T]
 
 
-@dataclass
+@dataclass(frozen=True)
 class BaseListCrown(Generic[T]):
     map: Sequence[T]
 
 
-@dataclass
+@dataclass(frozen=True)
 class BaseNoneCrown:
     pass
 
 
-@dataclass
+@dataclass(frozen=True)
 class BaseFieldCrown:
     id: str
 
@@ -65,22 +65,25 @@ class BaseFieldCrown:
 ListExtraPolicy = Union[ExtraSkip, ExtraForbid]
 
 
-@dataclass
+@dataclass(frozen=True)
 class InpDictCrown(BaseDictCrown["InpCrown"]):
     extra_policy: DictExtraPolicy
 
+    def __hash__(self):
+        return hash(MappingHashWrapper(self.map))
+
 
-@dataclass
+@dataclass(frozen=True)
 class InpListCrown(BaseListCrown["InpCrown"]):
     extra_policy: ListExtraPolicy
 
 
-@dataclass
+@dataclass(frozen=True)
 class InpNoneCrown(BaseNoneCrown):
     pass
 
 
-@dataclass
+@dataclass(frozen=True)
 class InpFieldCrown(BaseFieldCrown):
     pass
 
@@ -96,7 +99,7 @@ class InpFieldCrown(BaseFieldCrown):
 Sieve = Callable[[Any, Any], bool]
 
 
-@dataclass
+@dataclass(frozen=True)
 class OutDictCrown(BaseDictCrown["OutCrown"]):
     sieves: Dict[str, Sieve]
 
@@ -110,8 +113,11 @@ def _validate(self):
     def __post_init__(self):
         self._validate()
 
+    def __hash__(self):
+        return hash((MappingHashWrapper(self.map), MappingHashWrapper(self.sieves)))
+
 
-@dataclass
+@dataclass(frozen=True)
 class OutListCrown(BaseListCrown["OutCrown"]):
     pass
 
@@ -119,12 +125,12 @@ class OutListCrown(BaseListCrown["OutCrown"]):
 Placeholder = Union[DefaultValue, DefaultFactory]
 
 
-@dataclass
+@dataclass(frozen=True)
 class OutNoneCrown(BaseNoneCrown):
     placeholder: Placeholder
 
 
-@dataclass
+@dataclass(frozen=True)
 class OutFieldCrown(BaseFieldCrown):
     pass
 
diff --git a/src/adaptix/_internal/morphing/model/dumper_provider.py b/src/adaptix/_internal/morphing/model/dumper_provider.py
index a93e1ad3..6e0c91dc 100644
--- a/src/adaptix/_internal/morphing/model/dumper_provider.py
+++ b/src/adaptix/_internal/morphing/model/dumper_provider.py
@@ -10,13 +10,14 @@
 from ...provider.fields import output_field_to_loc
 from ...provider.located_request import LocatedRequest
 from ...provider.shape_provider import OutputShapeRequest, provide_generic_resolved_shape
-from ...utils import Omittable, Omitted
+from ...utils import AlwaysEqualHashWrapper, Omittable, Omitted, OrderedMappingHashWrapper
 from ..json_schema.definitions import JSONSchema
 from ..json_schema.request_cls import JSONSchemaRequest
 from ..json_schema.schema_model import JSONValue
 from ..provider_template import DumperProvider, JSONSchemaProvider
 from ..request_cls import DebugTrailRequest, DumperRequest
 from .basic_gen import (
+    CodeGenHook,
     ModelDumperGen,
     compile_closure_with_globals_capturing,
     fetch_code_gen_hook,
@@ -33,16 +34,49 @@ def __init__(self, *, name_sanitizer: NameSanitizer = BuiltinNameSanitizer()):
         self._name_sanitizer = name_sanitizer
 
     def provide_dumper(self, mediator: Mediator, request: DumperRequest) -> Dumper:
-        dumper_gen = self._fetch_model_dumper_gen(mediator, request)
-        closure_name = self._get_closure_name(request)
+        shape = self._fetch_shape(mediator, request)
+        name_layout = self._fetch_name_layout(mediator, request, shape)
+        fields_dumpers = self._fetch_field_dumpers(mediator, request, shape)
+        return mediator.cached_call(
+            self._make_dumper,
+            shape=shape,
+            name_layout=name_layout,
+            fields_dumpers=OrderedMappingHashWrapper(fields_dumpers),
+            debug_trail=mediator.mandatory_provide(DebugTrailRequest(loc_stack=request.loc_stack)),
+            code_gen_hook=AlwaysEqualHashWrapper(fetch_code_gen_hook(mediator, request.loc_stack)),
+            model_identity=self._fetch_model_identity(mediator, request, shape, name_layout),
+            closure_name=self._get_closure_name(request),
+            file_name=self._get_file_name(request),
+        )
+
+    def _make_dumper(
+        self,
+        *,
+        shape: OutputShape,
+        name_layout: OutputNameLayout,
+        fields_dumpers: OrderedMappingHashWrapper[Mapping[str, Dumper]],
+        debug_trail: DebugTrail,
+        code_gen_hook: AlwaysEqualHashWrapper[CodeGenHook],
+        model_identity: str,
+        closure_name: str,
+        file_name: str,
+    ) -> Dumper:
+        self._validate_params(shape, name_layout)
+        dumper_gen = self._create_model_dumper_gen(
+            debug_trail=debug_trail,
+            shape=shape,
+            name_layout=name_layout,
+            fields_dumpers=fields_dumpers.mapping,
+            model_identity=model_identity,
+        )
         dumper_code, dumper_namespace = dumper_gen.produce_code(closure_name=closure_name)
         return compile_closure_with_globals_capturing(
             compiler=self._get_compiler(),
-            code_gen_hook=fetch_code_gen_hook(mediator, request.loc_stack),
+            code_gen_hook=code_gen_hook.value,
             namespace=dumper_namespace,
             closure_code=dumper_code,
             closure_name=closure_name,
-            file_name=self._get_file_name(request),
+            file_name=file_name,
         )
 
     def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema:
diff --git a/src/adaptix/_internal/morphing/model/loader_provider.py b/src/adaptix/_internal/morphing/model/loader_provider.py
index 2ec73f0d..55aee231 100644
--- a/src/adaptix/_internal/morphing/model/loader_provider.py
+++ b/src/adaptix/_internal/morphing/model/loader_provider.py
@@ -10,7 +10,7 @@
 from ...provider.fields import input_field_to_loc
 from ...provider.located_request import LocatedRequest
 from ...provider.shape_provider import InputShapeRequest, provide_generic_resolved_shape
-from ...utils import Omittable, Omitted
+from ...utils import AlwaysEqualHashWrapper, Omittable, Omitted, OrderedMappingHashWrapper
 from ..json_schema.definitions import JSONSchema
 from ..json_schema.request_cls import JSONSchemaRequest
 from ..json_schema.schema_model import JSONValue
@@ -18,6 +18,7 @@
 from ..provider_template import JSONSchemaProvider, LoaderProvider
 from ..request_cls import DebugTrailRequest, DumperRequest, LoaderRequest, StrictCoercionRequest
 from .basic_gen import (
+    CodeGenHook,
     ModelLoaderGen,
     compile_closure_with_globals_capturing,
     fetch_code_gen_hook,
@@ -41,16 +42,54 @@ def __init__(
         self._props = props
 
     def provide_loader(self, mediator: Mediator, request: LoaderRequest) -> Loader:
-        loader_gen = self._fetch_model_loader_gen(mediator, request)
-        closure_name = self._get_closure_name(request)
+        shape = self._fetch_shape(mediator, request)
+        name_layout = self._fetch_name_layout(mediator, request, shape)
+        field_loaders = self._fetch_field_loaders(mediator, request, shape)
+        return mediator.cached_call(
+            self._make_loader,
+            shape=shape,
+            name_layout=name_layout,
+            field_loaders=OrderedMappingHashWrapper(field_loaders),
+            strict_coercion=mediator.mandatory_provide(StrictCoercionRequest(loc_stack=request.loc_stack)),
+            debug_trail=mediator.mandatory_provide(DebugTrailRequest(loc_stack=request.loc_stack)),
+            code_gen_hook=AlwaysEqualHashWrapper(fetch_code_gen_hook(mediator, request.loc_stack)),
+            model_identity=self._fetch_model_identity(mediator, request, shape, name_layout),
+            closure_name=self._get_closure_name(request),
+            file_name=self._get_file_name(request),
+        )
+
+    def _make_loader(
+        self,
+        *,
+        shape: InputShape,
+        name_layout: InputNameLayout,
+        field_loaders: OrderedMappingHashWrapper[Mapping[str, Loader]],
+        strict_coercion: bool,
+        debug_trail: DebugTrail,
+        code_gen_hook: AlwaysEqualHashWrapper[CodeGenHook],
+        model_identity: str,
+        closure_name: str,
+        file_name: str,
+    ) -> Loader:
+        skipped_fields = get_skipped_fields(shape, name_layout)
+        self._validate_params(shape, name_layout, skipped_fields)
+        loader_gen = self._create_model_loader_gen(
+            debug_trail=debug_trail,
+            strict_coercion=strict_coercion,
+            shape=shape,
+            name_layout=name_layout,
+            field_loaders=field_loaders.mapping,
+            skipped_fields=skipped_fields,
+            model_identity=model_identity,
+        )
         loader_code, loader_namespace = loader_gen.produce_code(closure_name=closure_name)
         return compile_closure_with_globals_capturing(
             compiler=self._get_compiler(),
-            code_gen_hook=fetch_code_gen_hook(mediator, request.loc_stack),
+            code_gen_hook=code_gen_hook.value,
             namespace=loader_namespace,
             closure_code=loader_code,
             closure_name=closure_name,
-            file_name=self._get_file_name(request),
+            file_name=file_name,
         )
 
     def _generate_json_schema(self, mediator: Mediator, request: JSONSchemaRequest) -> JSONSchema:
diff --git a/src/adaptix/_internal/morphing/name_layout/component.py b/src/adaptix/_internal/morphing/name_layout/component.py
index 068c47c0..0bd935ac 100644
--- a/src/adaptix/_internal/morphing/name_layout/component.py
+++ b/src/adaptix/_internal/morphing/name_layout/component.py
@@ -20,7 +20,6 @@
 from ...provider.located_request import LocatedRequest
 from ...provider.overlay_schema import Overlay, Schema, provide_schema
 from ...retort.operating_retort import OperatingRetort
-from ...retort.searching_retort import ProviderNotFoundError
 from ...special_cases_optimization import with_default_clause
 from ...utils import Omittable, get_prefix_groups
 from ..model.crown_definitions import (
@@ -105,7 +104,7 @@ def apply_lsc(
 
 class NameMappingRetort(OperatingRetort):
     def provide_name_mapping(self, request: NameMappingRequest) -> Optional[KeyPath]:
-        return self._facade_provide(request, error_message="")
+        return self._provide_from_recipe(request)
 
 
 class BuiltinStructureMaker(StructureMaker):
@@ -146,7 +145,7 @@ def _map_fields(
                         loc_stack=request.loc_stack.append_with(field_to_loc(field)),
                     ),
                 )
-            except ProviderNotFoundError:
+            except CannotProvide:
                 path = (generated_key, )
 
             if path is None:
diff --git a/src/adaptix/_internal/morphing/name_layout/crown_builder.py b/src/adaptix/_internal/morphing/name_layout/crown_builder.py
index e33692ba..fbdfc02d 100644
--- a/src/adaptix/_internal/morphing/name_layout/crown_builder.py
+++ b/src/adaptix/_internal/morphing/name_layout/crown_builder.py
@@ -97,10 +97,10 @@ def _get_list_crown_map(
         ]
         if paths_with_leaves and len(grouped_paths) != cast(int, paths_with_leaves[-1].path[len(current_path)]) + 1:
             raise ValueError(f"Found gaps in list mapping at {current_path}")
-        return [
+        return tuple(
             self._build_crown(path_group, len(current_path) + 1)
             for path_group in grouped_paths
-        ]
+        )
 
     @abstractmethod
     def _make_list_crown(self, current_path: KeyPath, paths_with_leaves: PathedLeaves[LeafCr]) -> ListCr:
diff --git a/src/adaptix/_internal/provider/essential.py b/src/adaptix/_internal/provider/essential.py
index 3037c88c..c8589884 100644
--- a/src/adaptix/_internal/provider/essential.py
+++ b/src/adaptix/_internal/provider/essential.py
@@ -1,6 +1,7 @@
+import typing
 from abc import ABC, abstractmethod
 from dataclasses import dataclass
-from typing import Any, Callable, Generic, Iterable, Optional, Sequence, Tuple, Type, TypeVar, final
+from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Optional, Sequence, Tuple, Type, TypeVar, final
 
 from ..common import VarTuple
 from ..compat import CompatExceptionGroup
@@ -206,6 +207,10 @@ def mandatory_apply_by_iterable(
 ResponseT = TypeVar("ResponseT")
 
 
+if TYPE_CHECKING:
+    P = typing.ParamSpec("P")
+
+
 class Mediator(DirectMediator, ABC, Generic[ResponseT]):
     """Mediator is an object that gives provider access to other providers
     and that stores the state of the current search.
@@ -219,6 +224,15 @@ def provide_from_next(self) -> ResponseT:
         that placed after current provider at the recipe.
         """
 
+    if TYPE_CHECKING:
+        @abstractmethod
+        def cached_call(self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
+            ...
+    else:
+        @abstractmethod
+        def cached_call(self, func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
+            ...
+
 
 RequestT = TypeVar("RequestT", bound=Request)
 RequestHandler = Callable[[Mediator[ResponseT], RequestT], ResponseT]
diff --git a/src/adaptix/_internal/provider/shape_provider.py b/src/adaptix/_internal/provider/shape_provider.py
index 84a5266d..3032ba91 100644
--- a/src/adaptix/_internal/provider/shape_provider.py
+++ b/src/adaptix/_internal/provider/shape_provider.py
@@ -59,14 +59,14 @@ def _get_shape(self, tp) -> Shape:
 
     @method_handler
     def _provide_input_shape(self, mediator: Mediator, request: InputShapeRequest) -> InputShape:
-        shape = self._get_shape(request.last_loc.type)
+        shape = mediator.cached_call(self._get_shape, request.last_loc.type)
         if shape.input is None:
             raise CannotProvide
         return shape.input
 
     @method_handler
     def _provide_output_shape(self, mediator: Mediator, request: OutputShapeRequest) -> OutputShape:
-        shape = self._get_shape(request.last_loc.type)
+        shape = mediator.cached_call(self._get_shape, request.last_loc.type)
         if shape.output is None:
             raise CannotProvide
         return shape.output
diff --git a/src/adaptix/_internal/retort/builtin_mediator.py b/src/adaptix/_internal/retort/builtin_mediator.py
index 424b64b9..9667b8b6 100644
--- a/src/adaptix/_internal/retort/builtin_mediator.py
+++ b/src/adaptix/_internal/retort/builtin_mediator.py
@@ -1,5 +1,5 @@
 from abc import ABC, abstractmethod
-from typing import Callable, Generic, Mapping, Type, TypeVar
+from typing import Any, Callable, Dict, Generic, Mapping, Type, TypeVar
 
 from ..provider.essential import CannotProvide, Mediator, Request
 
@@ -21,7 +21,7 @@ def send_chaining(self, request: RequestT, search_offset: int) -> ResponseT:
 
 
 class BuiltinMediator(Mediator[ResponseT], Generic[ResponseT]):
-    __slots__ = ("_request_buses", "_request", "_search_offset", "_no_request_bus_error_maker")
+    __slots__ = ("_request_buses", "_request", "_search_offset", "_no_request_bus_error_maker", "_call_cache")
 
     def __init__(
         self,
@@ -29,11 +29,15 @@ def __init__(
         request: Request,
         search_offset: int,
         no_request_bus_error_maker: Callable[[Request], CannotProvide],
+        call_cache: Dict[Any, Any],
     ):
         self._request_buses = request_buses
         self._request = request
         self._search_offset = search_offset
         self._no_request_bus_error_maker = no_request_bus_error_maker
+        self._call_cache = call_cache
+
+    __hash__ = None  # type: ignore[assignment]
 
     def provide(self, request: Request[T]) -> T:
         try:
@@ -45,3 +49,11 @@ def provide(self, request: Request[T]) -> T:
 
     def provide_from_next(self) -> ResponseT:
         return self._request_buses[type(self._request)].send_chaining(self._request, self._search_offset)
+
+    def cached_call(self, func: Callable[..., T], /, *args: Any, **kwargs: Any) -> T:  # type: ignore[override]
+        key = (func, *args, *kwargs.items())
+        if key in self._call_cache:
+            return self._call_cache[key]
+        result = func(*args, **kwargs)
+        self._call_cache[key] = result
+        return result
diff --git a/src/adaptix/_internal/retort/operating_retort.py b/src/adaptix/_internal/retort/operating_retort.py
index ca70c6fb..04bfb6e9 100644
--- a/src/adaptix/_internal/retort/operating_retort.py
+++ b/src/adaptix/_internal/retort/operating_retort.py
@@ -15,13 +15,22 @@
 
 
 class FuncWrapper:
-    __slots__ = ("__call__",)
+    __slots__ = ("__call__", "_key")
 
-    def __init__(self):
+    def __init__(self, key):
+        self._key = key
         self.__call__ = None
 
     def set_func(self, func):
-        self.__call__ = func.__call__
+        self.__call__ = func
+
+    def __eq__(self, other):
+        if isinstance(other, FuncWrapper):
+            return self._key == other._key
+        return NotImplemented
+
+    def __hash__(self):
+        return hash(self._key)
 
 
 CallableT = TypeVar("CallableT", bound=Callable)
@@ -32,16 +41,20 @@ def __init__(self) -> None:
         self._loc_to_stub: Dict[AnyLoc, FuncWrapper] = {}
 
     def track_request(self, request: LocatedRequest) -> Optional[Any]:
-        if request.loc_stack.count(request.last_loc) == 1:
+        last_loc = request.last_loc
+        if sum(loc == last_loc for loc in request.loc_stack) == 1:
             return None
 
-        stub = FuncWrapper()
-        self._loc_to_stub[request.last_loc] = stub
+        if last_loc in self._loc_to_stub:
+            return self._loc_to_stub[last_loc]
+        stub = FuncWrapper(last_loc)
+        self._loc_to_stub[last_loc] = stub
         return stub
 
     def track_response(self, request: LocatedRequest, response: CallableT) -> None:
-        if request.last_loc in self._loc_to_stub:
-            self._loc_to_stub.pop(request.last_loc).set_func(response)
+        last_loc = request.last_loc
+        if last_loc in self._loc_to_stub:
+            self._loc_to_stub.pop(last_loc).set_func(response)
 
 
 RequestT = TypeVar("RequestT", bound=Request)
diff --git a/src/adaptix/_internal/retort/searching_retort.py b/src/adaptix/_internal/retort/searching_retort.py
index 484aa3a1..8edeb1dc 100644
--- a/src/adaptix/_internal/retort/searching_retort.py
+++ b/src/adaptix/_internal/retort/searching_retort.py
@@ -1,6 +1,6 @@
 from abc import ABC, abstractmethod
 from collections import defaultdict
-from typing import Any, Callable, DefaultDict, List, Mapping, Optional, Sequence, Tuple, Type, TypeVar
+from typing import Any, Callable, DefaultDict, Dict, List, Mapping, Optional, Sequence, Tuple, Type, TypeVar
 
 from ..provider.essential import (
     AggregateCannotProvide,
@@ -36,7 +36,7 @@ class SearchingRetort(BaseRetort, Provider, ABC):
     """A retort that can operate as Retort but have no predefined providers and no high-level user interface"""
 
     def _provide_from_recipe(self, request: Request[T]) -> T:
-        return self._create_mediator(request).provide_from_next()
+        return self._create_mediator(request).provide(request)
 
     def get_request_handlers(self) -> Sequence[Tuple[Type[Request], RequestChecker, RequestHandler]]:
         def retort_request_handler(mediator, request):
@@ -90,6 +90,7 @@ def _calculate_derived(self) -> None:
             request_cls: self._create_error_representor(request_cls)
             for request_cls in self._request_cls_to_router
         }
+        self._call_cache: Dict[Any, Any] = {}
 
     def _create_request_cls_to_router(self, full_recipe: Sequence[Provider]) -> Mapping[Type[Request], RequestRouter]:
         request_cls_to_checkers_and_handlers: DefaultDict[Type[Request], List[CheckerAndHandler]] = defaultdict(list)
@@ -148,6 +149,7 @@ def no_request_bus_error_maker(request: Request) -> CannotProvide:
     def _create_mediator(self, init_request: Request[T]) -> Mediator[T]:
         request_buses: Mapping[Type[Request], RequestBus]
         no_request_bus_error_maker = self._create_no_request_bus_error_maker()
+        call_cache = self._call_cache
 
         def mediator_factory(request, search_offset):
             return BuiltinMediator(
@@ -155,6 +157,7 @@ def mediator_factory(request, search_offset):
                 request=request,
                 search_offset=search_offset,
                 no_request_bus_error_maker=no_request_bus_error_maker,
+                call_cache=call_cache,
             )
 
         request_buses = {
diff --git a/src/adaptix/_internal/utils.py b/src/adaptix/_internal/utils.py
index 2d246b45..b54df3c0 100644
--- a/src/adaptix/_internal/utils.py
+++ b/src/adaptix/_internal/utils.py
@@ -9,9 +9,11 @@
     Callable,
     Collection,
     Generator,
+    Generic,
     Iterable,
     Iterator,
     List,
+    Mapping,
     Protocol,
     Tuple,
     TypeVar,
@@ -232,3 +234,62 @@ def deprecated_alias_getter(name):
         return getattr(sys.modules[module_name], new_name)
 
     return deprecated_alias_getter
+
+
+MappingT = TypeVar("MappingT", bound=Mapping)
+
+
+class OrderedMappingHashWrapper(Generic[MappingT]):
+    __slots__ = ("mapping", "_hash")
+
+    def __init__(self, mapping: MappingT):
+        self.mapping = mapping
+        self._hash = hash(tuple(self.mapping.items()))
+
+    def __hash__(self):
+        return self._hash
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedMappingHashWrapper):
+            return self.mapping == other.mapping
+        return NotImplemented
+
+    def __repr__(self):
+        return f"OrderedMappingHashWrapper({self.mapping})"
+
+
+class MappingHashWrapper(Generic[MappingT]):
+    __slots__ = ("mapping", "_hash")
+
+    def __init__(self, mapping: MappingT):
+        self.mapping = mapping
+        self._hash = hash(frozenset(self.mapping.items()))
+
+    def __hash__(self):
+        return self._hash
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedMappingHashWrapper):
+            return self.mapping == other.mapping
+        return NotImplemented
+
+    def __repr__(self):
+        return f"MappingHashWrapper({self.mapping})"
+
+
+class AlwaysEqualHashWrapper(Generic[T]):
+    __slots__ = ("value", )
+
+    def __init__(self, value: T):
+        self.value = value
+
+    def __hash__(self):
+        return 0
+
+    def __eq__(self, other):
+        if isinstance(other, AlwaysEqualHashWrapper):
+            return True
+        return NotImplemented
+
+    def __repr__(self):
+        return f"AlwaysEqualHashWrapper({self.value})"


Test patch:
diff --git a/tests/unit/morphing/model/test_dumper_provider.py b/tests/unit/morphing/model/test_dumper_provider.py
index 0936f991..5abd7b30 100644
--- a/tests/unit/morphing/model/test_dumper_provider.py
+++ b/tests/unit/morphing/model/test_dumper_provider.py
@@ -820,10 +820,10 @@ def test_optional_fields_at_list(debug_ctx, debug_trail, acc_schema):
         ),
         name_layout=OutputNameLayout(
             crown=OutListCrown(
-                [
+                (
                     OutFieldCrown("a"),
                     OutFieldCrown("b"),
-                ],
+                ),
             ),
             extra_move=None,
         ),
@@ -993,10 +993,10 @@ def test_direct_list(debug_ctx, debug_trail, trail_select, acc_schema):
         ),
         name_layout=OutputNameLayout(
             crown=OutListCrown(
-                [
+                (
                     OutFieldCrown("a"),
                     OutFieldCrown("b"),
-                ],
+                ),
             ),
             extra_move=None,
         ),
@@ -1084,7 +1084,7 @@ def test_structure_flattening(debug_ctx, debug_trail, trail_select, acc_schema):
                     ),
                     "w": OutFieldCrown("c"),
                     "v": OutListCrown(
-                        [
+                        (
                             OutFieldCrown("d"),
                             OutDictCrown(
                                 {
@@ -1093,11 +1093,11 @@ def test_structure_flattening(debug_ctx, debug_trail, trail_select, acc_schema):
                                 sieves={},
                             ),
                             OutListCrown(
-                                [
+                                (
                                     OutFieldCrown("f"),
-                                ],
+                                ),
                             ),
-                        ],
+                        ),
                     ),
                     "t": OutDictCrown(
                         {
@@ -1106,9 +1106,9 @@ def test_structure_flattening(debug_ctx, debug_trail, trail_select, acc_schema):
                         sieves={},
                     ),
                     "r": OutListCrown(
-                        [
+                        (
                             OutFieldCrown("h"),
-                        ],
+                        ),
                     ),
                 },
                 sieves={
@@ -1362,12 +1362,12 @@ def test_none_crown_at_list_crown(debug_ctx, debug_trail, acc_schema):
         ),
         name_layout=OutputNameLayout(
             crown=OutListCrown(
-                [
+                (
                     OutNoneCrown(placeholder=DefaultValue(None)),
                     OutNoneCrown(placeholder=DefaultValue(SomeClass(2))),
                     OutFieldCrown("a"),
                     OutNoneCrown(placeholder=DefaultFactory(list)),
-                ],
+                ),
             ),
             extra_move=None,
         ),
diff --git a/tests/unit/morphing/model/test_loader_provider.py b/tests/unit/morphing/model/test_loader_provider.py
index e55a954a..7c592e75 100644
--- a/tests/unit/morphing/model/test_loader_provider.py
+++ b/tests/unit/morphing/model/test_loader_provider.py
@@ -298,10 +298,10 @@ def test_direct_list(debug_ctx, debug_trail, extra_policy, trail_select, strict_
         ),
         name_layout=InputNameLayout(
             crown=InpListCrown(
-                [
+                (
                     InpFieldCrown("a"),
                     InpFieldCrown("b"),
-                ],
+                ),
                 extra_policy=extra_policy,
             ),
             extra_move=None,
@@ -710,10 +710,10 @@ def test_optional_fields_at_list(debug_ctx, debug_trail, extra_policy):
         ),
         name_layout=InputNameLayout(
             crown=InpListCrown(
-                [
+                (
                     InpFieldCrown("a"),
                     InpFieldCrown("b"),
-                ],
+                ),
                 extra_policy=extra_policy,
             ),
             extra_move=None,
@@ -785,7 +785,7 @@ def test_flat_mapping(debug_ctx, debug_trail, is_required, trail_select):
         ),
         "w": InpFieldCrown("c"),
         "v": InpListCrown(
-            [
+            (
                 InpFieldCrown("d"),
                 InpDictCrown(
                     {
@@ -794,12 +794,12 @@ def test_flat_mapping(debug_ctx, debug_trail, is_required, trail_select):
                     extra_policy=ExtraCollect(),
                 ),
                 InpListCrown(
-                    [
+                    (
                         InpFieldCrown("f"),
-                    ],
+                    ),
                     extra_policy=ExtraForbid(),
                 ),
-            ],
+            ),
             extra_policy=ExtraForbid(),
         ),
     },
@@ -1037,11 +1037,11 @@ def test_none_crown_at_list_crown(debug_ctx, debug_trail, extra_policy, trail_se
         ),
         name_layout=InputNameLayout(
             crown=InpListCrown(
-                [
+                (
                     InpNoneCrown(),
                     InpFieldCrown("a"),
                     InpNoneCrown(),
-                ],
+                ),
                 extra_policy=extra_policy,
             ),
             extra_move=None,
@@ -1184,7 +1184,7 @@ def test_empty_list(debug_ctx, debug_trail, extra_policy, trail_select, strict_c
         shape=shape(),
         name_layout=InputNameLayout(
             crown=InpListCrown(
-                [],
+                (),
                 extra_policy=extra_policy,
             ),
             extra_move=None,
diff --git a/tests/unit/morphing/name_layout/test_provider.py b/tests/unit/morphing/name_layout/test_provider.py
index 2fdf31e4..6a1b68d4 100644
--- a/tests/unit/morphing/name_layout/test_provider.py
+++ b/tests/unit/morphing/name_layout/test_provider.py
@@ -351,20 +351,20 @@ def test_as_list():
     ) == Layouts(
         inp=InputNameLayout(
             crown=InpListCrown(
-                map=[
+                map=(
                     InpFieldCrown(id="a"),
                     InpFieldCrown(id="b"),
-                ],
+                ),
                 extra_policy=ExtraSkip(),
             ),
             extra_move=None,
         ),
         out=OutputNameLayout(
             crown=OutListCrown(
-                map=[
+                map=(
                     OutFieldCrown(id="a"),
                     OutFieldCrown(id="b"),
-                ],
+                ),
             ),
             extra_move=None,
         ),
@@ -385,22 +385,22 @@ def test_as_list():
     ) == Layouts(
         inp=InputNameLayout(
             crown=InpListCrown(
-                map=[
+                map=(
                     InpFieldCrown(id="b"),
                     InpFieldCrown(id="a"),
                     InpFieldCrown(id="c"),
-                ],
+                ),
                 extra_policy=ExtraSkip(),
             ),
             extra_move=None,
         ),
         out=OutputNameLayout(
             crown=OutListCrown(
-                map=[
+                map=(
                     OutFieldCrown(id="b"),
                     OutFieldCrown(id="a"),
                     OutFieldCrown(id="c"),
-                ],
+                ),
             ),
             extra_move=None,
         ),
@@ -544,22 +544,22 @@ def test_gaps_filling():
     ) == Layouts(
         inp=InputNameLayout(
             crown=InpListCrown(
-                map=[
+                map=(
                     InpFieldCrown(id="a"),
                     InpNoneCrown(),
                     InpFieldCrown(id="b"),
-                ],
+                ),
                 extra_policy=ExtraSkip(),
             ),
             extra_move=None,
         ),
         out=OutputNameLayout(
             crown=OutListCrown(
-                map=[
+                map=(
                     OutFieldCrown(id="a"),
                     OutNoneCrown(placeholder=DefaultValue(value=None)),
                     OutFieldCrown(id="b"),
-                ],
+                ),
             ),
             extra_move=None,
         ),
@@ -590,19 +590,19 @@ def test_structure_flattening():
                 map={
                     "f": InpFieldCrown(id="f"),
                     "w": InpListCrown(
-                        map=[
+                        map=(
                             InpFieldCrown(id="d"),
-                        ],
+                        ),
                         extra_policy=ExtraSkip(),
                     ),
                     "x": InpDictCrown(
                         map={
                             "e": InpFieldCrown(id="e"),
                             "y": InpListCrown(
-                                map=[
+                                map=(
                                     InpFieldCrown(id="a"),
                                     InpFieldCrown(id="b"),
-                                ],
+                                ),
                                 extra_policy=ExtraSkip(),
                             ),
                             "z": InpFieldCrown(id="c"),
@@ -619,18 +619,18 @@ def test_structure_flattening():
                 map={
                     "f": OutFieldCrown(id="f"),
                     "w": OutListCrown(
-                        map=[
+                        map=(
                             OutFieldCrown(id="d"),
-                        ],
+                        ),
                     ),
                     "x": OutDictCrown(
                         map={
                             "e": OutFieldCrown(id="e"),
                             "y": OutListCrown(
-                                map=[
+                                map=(
                                     OutFieldCrown(id="a"),
                                     OutFieldCrown(id="b"),
-                                ],
+                                ),
                             ),
                             "z": OutFieldCrown(id="c"),
                         },
@@ -832,18 +832,18 @@ def test_extra_at_list():
     assert layouts == Layouts(
         InputNameLayout(
             crown=InpListCrown(
-                map=[
+                map=(
                     InpFieldCrown("a"),
-                ],
+                ),
                 extra_policy=ExtraSkip(),
             ),
             extra_move=None,
         ),
         OutputNameLayout(
             crown=OutListCrown(
-                map=[
+                map=(
                     OutFieldCrown("a"),
-                ],
+                ),
             ),
             extra_move=None,
         ),
@@ -863,18 +863,18 @@ def test_extra_at_list():
     assert layouts == Layouts(
         InputNameLayout(
             crown=InpListCrown(
-                map=[
+                map=(
                     InpFieldCrown("a"),
-                ],
+                ),
                 extra_policy=ExtraForbid(),
             ),
             extra_move=None,
         ),
         OutputNameLayout(
             crown=OutListCrown(
-                map=[
+                map=(
                     OutFieldCrown("a"),
-                ],
+                ),
             ),
             extra_move=None,
         ),
@@ -1205,9 +1205,9 @@ def test_ellipsis_replacing_int_key():
         TestField("b"),
         name_mapping(
             as_list=True,
-            map=[
+            map=(
                 (".*", ("data", ...)),
-            ],
+            ),
         ),
         DEFAULT_NAME_MAPPING,
     )
@@ -1216,10 +1216,10 @@ def test_ellipsis_replacing_int_key():
             crown=InpDictCrown(
                 map={
                     "data": InpListCrown(
-                        map=[
+                        map=(
                             InpFieldCrown("a_"),
                             InpFieldCrown("b"),
-                        ],
+                        ),
                         extra_policy=ExtraSkip(),
                     ),
                 },
@@ -1231,10 +1231,10 @@ def test_ellipsis_replacing_int_key():
             crown=OutDictCrown(
                 map={
                     "data": OutListCrown(
-                        map=[
+                        map=(
                             OutFieldCrown("a_"),
                             OutFieldCrown("b"),
-                        ],
+                        ),
                     ),
                 },
                 sieves={},
@@ -1274,14 +1274,14 @@ def test_empty_models_list():
     assert layouts == Layouts(
         InputNameLayout(
             crown=InpListCrown(
-                map=[],
+                map=(),
                 extra_policy=ExtraSkip(),
             ),
             extra_move=None,
         ),
         OutputNameLayout(
             crown=OutListCrown(
-                map=[],
+                map=(),
             ),
             extra_move=None,
         ),