egglog 12.0.0__cp313-cp313t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- egglog/__init__.py +13 -0
- egglog/bindings.cpython-313t-powerpc64-linux-gnu.so +0 -0
- egglog/bindings.pyi +887 -0
- egglog/builtins.py +1144 -0
- egglog/config.py +8 -0
- egglog/conversion.py +290 -0
- egglog/declarations.py +964 -0
- egglog/deconstruct.py +176 -0
- egglog/egraph.py +2247 -0
- egglog/egraph_state.py +978 -0
- egglog/examples/README.rst +5 -0
- egglog/examples/__init__.py +3 -0
- egglog/examples/bignum.py +32 -0
- egglog/examples/bool.py +38 -0
- egglog/examples/eqsat_basic.py +44 -0
- egglog/examples/fib.py +28 -0
- egglog/examples/higher_order_functions.py +42 -0
- egglog/examples/jointree.py +64 -0
- egglog/examples/lambda_.py +287 -0
- egglog/examples/matrix.py +175 -0
- egglog/examples/multiset.py +60 -0
- egglog/examples/ndarrays.py +144 -0
- egglog/examples/resolution.py +84 -0
- egglog/examples/schedule_demo.py +34 -0
- egglog/exp/MoA.ipynb +617 -0
- egglog/exp/__init__.py +3 -0
- egglog/exp/any_expr.py +947 -0
- egglog/exp/any_expr_example.ipynb +408 -0
- egglog/exp/array_api.py +2019 -0
- egglog/exp/array_api_jit.py +51 -0
- egglog/exp/array_api_loopnest.py +74 -0
- egglog/exp/array_api_numba.py +69 -0
- egglog/exp/array_api_program_gen.py +510 -0
- egglog/exp/program_gen.py +427 -0
- egglog/exp/siu_examples.py +32 -0
- egglog/ipython_magic.py +41 -0
- egglog/pretty.py +566 -0
- egglog/py.typed +0 -0
- egglog/runtime.py +888 -0
- egglog/thunk.py +97 -0
- egglog/type_constraint_solver.py +111 -0
- egglog/visualizer.css +1 -0
- egglog/visualizer.js +35798 -0
- egglog/visualizer_widget.py +39 -0
- egglog-12.0.0.dist-info/METADATA +93 -0
- egglog-12.0.0.dist-info/RECORD +48 -0
- egglog-12.0.0.dist-info/WHEEL +5 -0
- egglog-12.0.0.dist-info/licenses/LICENSE +21 -0
egglog/runtime.py
ADDED
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Holds a number of types which are only used at runtime to emulate Python objects.
|
|
3
|
+
|
|
4
|
+
Users will not import anything from this module, and statically they won't know these are the types they are using.
|
|
5
|
+
|
|
6
|
+
But at runtime they will be exposed.
|
|
7
|
+
|
|
8
|
+
Note that all their internal fields are prefixed with __egg_ to avoid name collisions with user code, but will end in __
|
|
9
|
+
so they are not mangled by Python and can be accessed by the user.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import itertools
|
|
15
|
+
import operator
|
|
16
|
+
import types
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from dataclasses import InitVar, dataclass, replace
|
|
19
|
+
from inspect import Parameter, Signature
|
|
20
|
+
from itertools import zip_longest
|
|
21
|
+
from typing import TYPE_CHECKING, Any, TypeVar, Union, assert_never, cast, get_args, get_origin
|
|
22
|
+
|
|
23
|
+
import cloudpickle
|
|
24
|
+
|
|
25
|
+
from .declarations import *
|
|
26
|
+
from .pretty import *
|
|
27
|
+
from .thunk import Thunk
|
|
28
|
+
from .type_constraint_solver import *
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Iterable
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"ALWAYS_MUTATES_SELF",
|
|
36
|
+
"ALWAYS_PRESERVED",
|
|
37
|
+
"LIT_IDENTS",
|
|
38
|
+
"NUMERIC_BINARY_METHODS",
|
|
39
|
+
"RuntimeClass",
|
|
40
|
+
"RuntimeExpr",
|
|
41
|
+
"RuntimeFunction",
|
|
42
|
+
"create_callable",
|
|
43
|
+
"define_expr_method",
|
|
44
|
+
"resolve_callable",
|
|
45
|
+
"resolve_type_annotation",
|
|
46
|
+
"resolve_type_annotation_mutate",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
UNIT_IDENT = Ident.builtin("Unit")
|
|
51
|
+
UNARY_LIT_IDENTS = {Ident.builtin("i64"), Ident.builtin("f64"), Ident.builtin("Bool"), Ident.builtin("String")}
|
|
52
|
+
LIT_IDENTS = UNARY_LIT_IDENTS | {UNIT_IDENT, Ident.builtin("PyObject")}
|
|
53
|
+
|
|
54
|
+
# All methods which should return NotImplemented if they fail to resolve and are reflected as well
|
|
55
|
+
# From https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types
|
|
56
|
+
|
|
57
|
+
NUMERIC_BINARY_METHODS = {
|
|
58
|
+
"__add__",
|
|
59
|
+
"__sub__",
|
|
60
|
+
"__mul__",
|
|
61
|
+
"__matmul__",
|
|
62
|
+
"__truediv__",
|
|
63
|
+
"__floordiv__",
|
|
64
|
+
"__mod__",
|
|
65
|
+
"__divmod__",
|
|
66
|
+
"__pow__",
|
|
67
|
+
"__lshift__",
|
|
68
|
+
"__rshift__",
|
|
69
|
+
"__and__",
|
|
70
|
+
"__xor__",
|
|
71
|
+
"__or__",
|
|
72
|
+
"__lt__",
|
|
73
|
+
"__le__",
|
|
74
|
+
"__gt__",
|
|
75
|
+
"__ge__",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# special methods that return none and mutate self
|
|
80
|
+
ALWAYS_MUTATES_SELF = {
|
|
81
|
+
"__setitem__",
|
|
82
|
+
"__delitem__",
|
|
83
|
+
# "__setattr__",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# special methods which must return real python values instead of lazy expressions
|
|
87
|
+
ALWAYS_PRESERVED = {
|
|
88
|
+
"__bytes__",
|
|
89
|
+
# "__format__",
|
|
90
|
+
"__hash__",
|
|
91
|
+
"__bool__",
|
|
92
|
+
"__len__",
|
|
93
|
+
"__length_hint__",
|
|
94
|
+
"__iter__",
|
|
95
|
+
"__reversed__",
|
|
96
|
+
"__contains__",
|
|
97
|
+
"__index__",
|
|
98
|
+
"__buffer__",
|
|
99
|
+
"__complex__",
|
|
100
|
+
"__int__",
|
|
101
|
+
"__float__",
|
|
102
|
+
"__str__",
|
|
103
|
+
"__repr__",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Methods that need to be defined on the runtime type that holds `Expr` objects, so that they can be used as methods.
|
|
107
|
+
|
|
108
|
+
TYPE_DEFINED_METHODS = {
|
|
109
|
+
"__call__",
|
|
110
|
+
"__getitem__",
|
|
111
|
+
"__pos__",
|
|
112
|
+
"__neg__",
|
|
113
|
+
"__invert__",
|
|
114
|
+
"__round__",
|
|
115
|
+
"__abs__",
|
|
116
|
+
"__trunc__",
|
|
117
|
+
"__floor__",
|
|
118
|
+
"__ceil__",
|
|
119
|
+
*ALWAYS_PRESERVED,
|
|
120
|
+
*ALWAYS_MUTATES_SELF,
|
|
121
|
+
} - {"__str__", "__repr__"} # these are defined on RuntimeFunction
|
|
122
|
+
|
|
123
|
+
# Methods that need to be defined as descriptors on the class so that they can be looked up on the class itself.
|
|
124
|
+
CLASS_DESCRIPTOR_METHODS = {
|
|
125
|
+
"__eq__",
|
|
126
|
+
"__ne__",
|
|
127
|
+
"__str__",
|
|
128
|
+
"__repr__",
|
|
129
|
+
"__getattr__",
|
|
130
|
+
*NUMERIC_BINARY_METHODS,
|
|
131
|
+
*TYPE_DEFINED_METHODS,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Set this globally so we can get access to PyObject when we have a type annotation of just object.
|
|
135
|
+
# This is the only time a type annotation doesn't need to include the egglog type b/c object is top so that would be redundant statically.
|
|
136
|
+
_PY_OBJECT_CLASS: RuntimeClass | None = None
|
|
137
|
+
# Same for functions
|
|
138
|
+
_UNSTABLE_FN_CLASS: RuntimeClass | None = None
|
|
139
|
+
|
|
140
|
+
T = TypeVar("T")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def resolve_type_annotation_mutate(decls: Declarations, tp: object) -> TypeOrVarRef:
|
|
144
|
+
"""
|
|
145
|
+
Wrap resolve_type_annotation to mutate decls, as a helper for internal use in sitations where that is more ergonomic.
|
|
146
|
+
"""
|
|
147
|
+
new_decls, tp = resolve_type_annotation(tp)
|
|
148
|
+
decls |= new_decls
|
|
149
|
+
return tp
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def resolve_type_annotation(tp: object) -> tuple[DeclerationsLike, TypeOrVarRef]:
|
|
153
|
+
"""
|
|
154
|
+
Resolves a type object into a type reference.
|
|
155
|
+
|
|
156
|
+
Any runtime type object decls will be returned as well. We do this so we can use this without having to
|
|
157
|
+
resolve the decls if need be.
|
|
158
|
+
"""
|
|
159
|
+
if isinstance(tp, TypeVar):
|
|
160
|
+
return None, ClassTypeVarRef.from_type_var(tp)
|
|
161
|
+
# If there is a union, then we assume the first item is the type we want, and the others are types that can be converted to that type.
|
|
162
|
+
if get_origin(tp) == Union:
|
|
163
|
+
first, *_rest = get_args(tp)
|
|
164
|
+
return resolve_type_annotation(first)
|
|
165
|
+
|
|
166
|
+
# If the type is `object` then this is assumed to be a PyObjectLike, i.e. converted into a PyObject
|
|
167
|
+
if tp is object:
|
|
168
|
+
assert _PY_OBJECT_CLASS
|
|
169
|
+
return resolve_type_annotation(_PY_OBJECT_CLASS)
|
|
170
|
+
# If the type is a `Callable` then convert it into a UnstableFn
|
|
171
|
+
if get_origin(tp) == Callable:
|
|
172
|
+
assert _UNSTABLE_FN_CLASS
|
|
173
|
+
args, ret = get_args(tp)
|
|
174
|
+
return resolve_type_annotation(_UNSTABLE_FN_CLASS[(ret, *args)])
|
|
175
|
+
if isinstance(tp, RuntimeClass):
|
|
176
|
+
return tp, tp.__egg_tp__
|
|
177
|
+
raise TypeError(f"Unexpected type annotation {tp}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def inverse_resolve_type_annotation(decls_thunk: Callable[[], Declarations], tp: TypeOrVarRef) -> object:
|
|
181
|
+
"""
|
|
182
|
+
Inverse of resolve_type_annotation
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(tp, ClassTypeVarRef):
|
|
185
|
+
return tp.to_type_var()
|
|
186
|
+
return RuntimeClass(decls_thunk, tp)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
##
|
|
190
|
+
# Runtime objects
|
|
191
|
+
##
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class BaseClassFactoryMeta(type):
|
|
195
|
+
"""
|
|
196
|
+
Base metaclass for all runtime classes created by ClassFactory
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __instancecheck__(cls, instance: object) -> bool:
|
|
200
|
+
assert isinstance(cls, RuntimeClass)
|
|
201
|
+
return isinstance(instance, RuntimeExpr) and cls.__egg_tp__.ident == instance.__egg_typed_expr__.tp.ident
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ClassFactory(type):
|
|
205
|
+
"""
|
|
206
|
+
A metaclass for types which should create `type` objects when instantiated.
|
|
207
|
+
|
|
208
|
+
That's so that they work with `isinstance` and can be placed in `match ClassName()`.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def __call__(cls, *args, **kwargs) -> type:
|
|
212
|
+
# If we have params, don't inherit from `type` because we don't need to match against this and also
|
|
213
|
+
# this won't work with `Union[X]` because it won't look at `__parameters__` for instances of `type`.
|
|
214
|
+
if kwargs.pop("_egg_has_params", False):
|
|
215
|
+
return super().__call__(*args, **kwargs)
|
|
216
|
+
namespace: dict[str, Any] = {}
|
|
217
|
+
for m in reversed(cls.__mro__):
|
|
218
|
+
if m is not object:
|
|
219
|
+
namespace.update(m.__dict__)
|
|
220
|
+
init = namespace.pop("__init__")
|
|
221
|
+
meta = types.new_class("type(RuntimeClass)", (BaseClassFactoryMeta,), {}, lambda ns: ns.update(**namespace))
|
|
222
|
+
tp = types.new_class("RuntimeClass", (), {"metaclass": meta}, lambda ns: ns.update(RUNTIME_CLASS_DESCRIPTORS))
|
|
223
|
+
init(tp, *args, **kwargs)
|
|
224
|
+
return tp
|
|
225
|
+
|
|
226
|
+
def __instancecheck__(cls, instance: object) -> bool:
|
|
227
|
+
return isinstance(instance, BaseClassFactoryMeta)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Add a custom descriptor for each method we want to support on the class itself, so that we can lookup things like __add__
|
|
231
|
+
# on the expr class. This is used for things like running docstrings.
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class RuntimeClassDescriptor:
|
|
236
|
+
name: str
|
|
237
|
+
|
|
238
|
+
def __get__(self, obj: object, owner: RuntimeClass | None = None) -> Callable:
|
|
239
|
+
if owner is None:
|
|
240
|
+
raise AttributeError(f"Can only access {self.name} on the class, not an instance")
|
|
241
|
+
cls_decl = owner.__egg_decls__._classes[owner.__egg_tp__.ident]
|
|
242
|
+
if self.name in cls_decl.class_methods:
|
|
243
|
+
return RuntimeFunction(
|
|
244
|
+
owner.__egg_decls_thunk__, Thunk.value(ClassMethodRef(owner.__egg_tp__.ident, self.name)), None
|
|
245
|
+
)
|
|
246
|
+
if self.name in cls_decl.preserved_methods:
|
|
247
|
+
return cls_decl.preserved_methods[self.name]
|
|
248
|
+
raise AttributeError(f"Class {owner.__egg_tp__.ident} has no method {self.name}") from None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
RUNTIME_CLASS_DESCRIPTORS: dict[str, RuntimeClassDescriptor] = {
|
|
252
|
+
method: RuntimeClassDescriptor(method) for method in CLASS_DESCRIPTOR_METHODS
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(match_args=False)
|
|
257
|
+
class RuntimeClass(DelayedDeclerations, metaclass=ClassFactory):
|
|
258
|
+
__egg_tp__: TypeRefWithVars
|
|
259
|
+
# True if we want `__parameters__` to be recognized by `Union`, which means we can't inherit from `type` directly.
|
|
260
|
+
_egg_has_params: InitVar[bool] = False
|
|
261
|
+
|
|
262
|
+
def __post_init__(self, _egg_has_params: bool) -> None:
|
|
263
|
+
global _PY_OBJECT_CLASS, _UNSTABLE_FN_CLASS
|
|
264
|
+
ident = self.__egg_tp__.ident
|
|
265
|
+
if (ident) == Ident.builtin("PyObject"):
|
|
266
|
+
_PY_OBJECT_CLASS = self
|
|
267
|
+
elif ident == Ident.builtin("UnstableFn") and not self.__egg_tp__.args:
|
|
268
|
+
_UNSTABLE_FN_CLASS = self
|
|
269
|
+
# Set the module and name so that this works with doctest
|
|
270
|
+
if ident.module:
|
|
271
|
+
self.__module__ = ident.module
|
|
272
|
+
self.__name__ = ident.name
|
|
273
|
+
|
|
274
|
+
def verify(self) -> None:
|
|
275
|
+
if not self.__egg_tp__.args:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
# Raise error if we have args, but they are the wrong number
|
|
279
|
+
desired_args = self.__egg_decls__.get_class_decl(self.__egg_tp__.ident).type_vars
|
|
280
|
+
if len(self.__egg_tp__.args) != len(desired_args):
|
|
281
|
+
raise ValueError(f"Expected {desired_args} type args, got {len(self.__egg_tp__.args)}")
|
|
282
|
+
|
|
283
|
+
def __call__(self, *args: object, **kwargs: object) -> RuntimeExpr | None:
|
|
284
|
+
"""
|
|
285
|
+
Create an instance of this kind by calling the __init__ classmethod
|
|
286
|
+
"""
|
|
287
|
+
# If this is a literal type, initializing it with a literal should return a literal
|
|
288
|
+
if (name := self.__egg_tp__.ident) == Ident.builtin("PyObject"):
|
|
289
|
+
assert len(args) == 1
|
|
290
|
+
try:
|
|
291
|
+
pickled = cloudpickle.dumps(args[0])
|
|
292
|
+
except Exception as e:
|
|
293
|
+
e.add_note(f"Failed to pickle object of type {type(args[0])}")
|
|
294
|
+
raise
|
|
295
|
+
return RuntimeExpr(
|
|
296
|
+
self.__egg_decls_thunk__, Thunk.value(TypedExprDecl(self.__egg_tp__.to_just(), PyObjectDecl(pickled)))
|
|
297
|
+
)
|
|
298
|
+
if name == Ident.builtin("UnstableFn"):
|
|
299
|
+
assert not kwargs
|
|
300
|
+
fn_arg, *partial_args = args
|
|
301
|
+
del args
|
|
302
|
+
# Assumes we don't have types set for UnstableFn w/ generics, that they have to be inferred
|
|
303
|
+
|
|
304
|
+
# 1. Call it with the partial args, and use untyped vars for the rest of the args
|
|
305
|
+
res = cast("Callable", fn_arg)(*partial_args, _egg_partial_function=True)
|
|
306
|
+
assert res is not None, "Mutable partial functions not supported"
|
|
307
|
+
# 2. Use the inferred return type and inferred rest arg types as the types of the function, and
|
|
308
|
+
# the partially applied args as the args.
|
|
309
|
+
call = (res_typed_expr := res.__egg_typed_expr__).expr
|
|
310
|
+
return_tp = res_typed_expr.tp
|
|
311
|
+
assert isinstance(call, CallDecl), "partial function must be a call"
|
|
312
|
+
n_args = len(partial_args)
|
|
313
|
+
value = PartialCallDecl(replace(call, args=call.args[:n_args]))
|
|
314
|
+
remaining_arg_types = [a.tp for a in call.args[n_args:]]
|
|
315
|
+
type_ref = JustTypeRef(Ident.builtin("UnstableFn"), (return_tp, *remaining_arg_types))
|
|
316
|
+
return RuntimeExpr.__from_values__(Declarations.create(self, res), TypedExprDecl(type_ref, value))
|
|
317
|
+
|
|
318
|
+
if name in UNARY_LIT_IDENTS:
|
|
319
|
+
assert len(args) == 1
|
|
320
|
+
assert isinstance(args[0], int | float | str | bool)
|
|
321
|
+
return RuntimeExpr(
|
|
322
|
+
self.__egg_decls_thunk__, Thunk.value(TypedExprDecl(self.__egg_tp__.to_just(), LitDecl(args[0])))
|
|
323
|
+
)
|
|
324
|
+
if name == UNIT_IDENT:
|
|
325
|
+
assert len(args) == 0
|
|
326
|
+
return RuntimeExpr(
|
|
327
|
+
self.__egg_decls_thunk__, Thunk.value(TypedExprDecl(self.__egg_tp__.to_just(), LitDecl(None)))
|
|
328
|
+
)
|
|
329
|
+
fn = RuntimeFunction(self.__egg_decls_thunk__, Thunk.value(InitRef(name)), self.__egg_tp__.to_just())
|
|
330
|
+
return fn(*args, **kwargs) # type: ignore[arg-type]
|
|
331
|
+
|
|
332
|
+
def __dir__(self) -> Iterable[str]:
|
|
333
|
+
cls_decl = self.__egg_decls__._classes[self.__egg_tp__.ident]
|
|
334
|
+
return (
|
|
335
|
+
list(cls_decl.class_methods)
|
|
336
|
+
+ list(cls_decl.class_variables)
|
|
337
|
+
+ list(cls_decl.preserved_methods)
|
|
338
|
+
+ list(cls_decl.methods)
|
|
339
|
+
+ list(cls_decl.properties)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def __getitem__(self, args: object) -> RuntimeClass:
|
|
343
|
+
if not isinstance(args, tuple):
|
|
344
|
+
args = (args,)
|
|
345
|
+
# defer resolving decls so that we can do generic instantiation for converters before all
|
|
346
|
+
# method types are defined.
|
|
347
|
+
decls_like, new_args = cast(
|
|
348
|
+
"tuple[tuple[DeclerationsLike, ...], tuple[TypeOrVarRef, ...]]",
|
|
349
|
+
zip(*(resolve_type_annotation(arg) for arg in args), strict=False),
|
|
350
|
+
)
|
|
351
|
+
# if we already have some args bound and some not, then we shold replace all existing args of typevars with new
|
|
352
|
+
# args
|
|
353
|
+
if old_args := self.__egg_tp__.args:
|
|
354
|
+
is_typevar = [isinstance(arg, ClassTypeVarRef) for arg in old_args]
|
|
355
|
+
if sum(is_typevar) != len(new_args):
|
|
356
|
+
raise TypeError(f"Expected {sum(is_typevar)} typevars, got {len(new_args)}")
|
|
357
|
+
new_args_list = list(new_args)
|
|
358
|
+
final_args = tuple(new_args_list.pop(0) if is_typevar[i] else old_args[i] for i in range(len(old_args)))
|
|
359
|
+
else:
|
|
360
|
+
final_args = new_args
|
|
361
|
+
tp = TypeRefWithVars(self.__egg_tp__.ident, final_args)
|
|
362
|
+
return RuntimeClass(Thunk.fn(Declarations.create, self, *decls_like), tp, _egg_has_params=True)
|
|
363
|
+
|
|
364
|
+
def __getattr__(self, name: str) -> RuntimeFunction | RuntimeExpr | Callable:
|
|
365
|
+
if not isinstance(name, str):
|
|
366
|
+
raise TypeError(f"Attribute name must be a string, got {name!r}")
|
|
367
|
+
if name == "__origin__" and self.__egg_tp__.args:
|
|
368
|
+
return RuntimeClass(self.__egg_decls_thunk__, TypeRefWithVars(self.__egg_tp__.ident))
|
|
369
|
+
|
|
370
|
+
# Special case some names that don't exist so we can exit early without resolving decls
|
|
371
|
+
# Important so if we take union of RuntimeClass it won't try to resolve decls
|
|
372
|
+
if name in {
|
|
373
|
+
"__typing_subst__",
|
|
374
|
+
"__parameters__",
|
|
375
|
+
# Origin is used in get_type_hints which is used when resolving the class itself
|
|
376
|
+
"__origin__",
|
|
377
|
+
"__typing_unpacked_tuple_args__",
|
|
378
|
+
"__typing_is_unpacked_typevartuple__",
|
|
379
|
+
}:
|
|
380
|
+
raise AttributeError
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
cls_decl = self.__egg_decls__._classes[self.__egg_tp__.ident]
|
|
384
|
+
except Exception as e:
|
|
385
|
+
e.add_note(f"Error processing class {self.__egg_tp__.ident}")
|
|
386
|
+
raise
|
|
387
|
+
|
|
388
|
+
preserved_methods = cls_decl.preserved_methods
|
|
389
|
+
if name in preserved_methods:
|
|
390
|
+
return preserved_methods[name]
|
|
391
|
+
|
|
392
|
+
# if this is a class variable, return an expr for it, otherwise, assume it's a method
|
|
393
|
+
if name in cls_decl.class_variables:
|
|
394
|
+
return_tp = cls_decl.class_variables[name]
|
|
395
|
+
return RuntimeExpr(
|
|
396
|
+
self.__egg_decls_thunk__,
|
|
397
|
+
Thunk.value(TypedExprDecl(return_tp.type_ref, CallDecl(ClassVariableRef(self.__egg_tp__.ident, name)))),
|
|
398
|
+
)
|
|
399
|
+
if name in cls_decl.class_methods:
|
|
400
|
+
return RuntimeFunction(
|
|
401
|
+
self.__egg_decls_thunk__,
|
|
402
|
+
Thunk.value(ClassMethodRef(self.__egg_tp__.ident, name)),
|
|
403
|
+
self.__egg_tp__.to_just(),
|
|
404
|
+
)
|
|
405
|
+
# allow referencing properties and methods as class variables as well
|
|
406
|
+
if name in cls_decl.properties:
|
|
407
|
+
return RuntimeFunction(self.__egg_decls_thunk__, Thunk.value(PropertyRef(self.__egg_tp__.ident, name)))
|
|
408
|
+
if name in cls_decl.methods:
|
|
409
|
+
return RuntimeFunction(self.__egg_decls_thunk__, Thunk.value(MethodRef(self.__egg_tp__.ident, name)))
|
|
410
|
+
|
|
411
|
+
msg = f"Class {self.__egg_tp__.ident} has no method {name}"
|
|
412
|
+
raise AttributeError(msg) from None
|
|
413
|
+
|
|
414
|
+
def __str__(self) -> str:
|
|
415
|
+
return str(self.__egg_tp__)
|
|
416
|
+
|
|
417
|
+
def __repr__(self) -> str:
|
|
418
|
+
return str(self)
|
|
419
|
+
|
|
420
|
+
# Make hashable so can go in Union
|
|
421
|
+
def __hash__(self) -> int:
|
|
422
|
+
return hash(self.__egg_tp__)
|
|
423
|
+
|
|
424
|
+
def __eq__(self, other: object) -> bool:
|
|
425
|
+
"""
|
|
426
|
+
Support equality for runtime comparison of egglog classes.
|
|
427
|
+
"""
|
|
428
|
+
if not isinstance(other, RuntimeClass):
|
|
429
|
+
return NotImplemented
|
|
430
|
+
return self.__egg_tp__ == other.__egg_tp__
|
|
431
|
+
|
|
432
|
+
# Support unioning like types
|
|
433
|
+
def __or__(self, value: type) -> object:
|
|
434
|
+
return Union[self, value] # noqa: UP007
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def __parameters__(self) -> tuple[object, ...]:
|
|
438
|
+
"""
|
|
439
|
+
Emit a number of typevar params so that when using generic type aliases, we know how to resolve these properly.
|
|
440
|
+
"""
|
|
441
|
+
return tuple(inverse_resolve_type_annotation(self.__egg_decls_thunk__, tp) for tp in self.__egg_tp__.args)
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def __match_args__(self) -> tuple[str, ...]:
|
|
445
|
+
return self.__egg_decls__._classes[self.__egg_tp__.ident].match_args
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def __doc__(self) -> str | None: # type: ignore[override]
|
|
449
|
+
return self.__egg_decls__._classes[self.__egg_tp__.ident].doc
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def __dict__(self) -> dict[str, Any]: # type: ignore[override]
|
|
453
|
+
"""
|
|
454
|
+
Return the dict so that this works with things like `DocTestFinder`.
|
|
455
|
+
"""
|
|
456
|
+
return {attr: getattr(self, attr) for attr in dir(self)}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class RuntimeFunctionMeta(type):
|
|
460
|
+
# Override getatribute on class so that it if we call RuntimeFunction.__module__ we get this module
|
|
461
|
+
# but if we call it on an instance we get the module of the instance so that it works with doctest.
|
|
462
|
+
def __getattribute__(cls, name: str) -> Any:
|
|
463
|
+
if name == "__module__":
|
|
464
|
+
return __name__
|
|
465
|
+
return super().__getattribute__(name)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@dataclass
|
|
469
|
+
class RuntimeFunction(DelayedDeclerations, metaclass=RuntimeFunctionMeta):
|
|
470
|
+
__egg_ref_thunk__: Callable[[], CallableRef]
|
|
471
|
+
# bound methods need to store RuntimeExpr not just TypedExprDecl, so they can mutate the expr if required on self
|
|
472
|
+
__egg_bound__: JustTypeRef | RuntimeExpr | None = None
|
|
473
|
+
|
|
474
|
+
__get__: None = None
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def __module__(self) -> str | None: # type: ignore[override]
|
|
478
|
+
ref = self.__egg_ref__
|
|
479
|
+
if isinstance(ref, UnnamedFunctionRef):
|
|
480
|
+
return None
|
|
481
|
+
return ref.ident.module
|
|
482
|
+
|
|
483
|
+
def __eq__(self, other: object) -> bool:
|
|
484
|
+
"""
|
|
485
|
+
Support equality for runtime comparison of egglog functions.
|
|
486
|
+
"""
|
|
487
|
+
if not isinstance(other, RuntimeFunction):
|
|
488
|
+
return NotImplemented
|
|
489
|
+
return self.__egg_ref__ == other.__egg_ref__ and bool(self.__egg_bound__ == other.__egg_bound__)
|
|
490
|
+
|
|
491
|
+
def __hash__(self) -> int:
|
|
492
|
+
return hash((self.__egg_ref__, self.__egg_bound__))
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def __egg_ref__(self) -> CallableRef:
|
|
496
|
+
return self.__egg_ref_thunk__()
|
|
497
|
+
|
|
498
|
+
def __call__(self, *args: object, _egg_partial_function: bool = False, **kwargs: object) -> RuntimeExpr | None:
|
|
499
|
+
from .conversion import resolve_literal # noqa: PLC0415
|
|
500
|
+
|
|
501
|
+
if isinstance(self.__egg_bound__, RuntimeExpr):
|
|
502
|
+
args = (self.__egg_bound__, *args)
|
|
503
|
+
try:
|
|
504
|
+
signature = self.__egg_decls__.get_callable_decl(self.__egg_ref__).signature
|
|
505
|
+
except Exception as e:
|
|
506
|
+
e.add_note(f"Failed to find callable {self}")
|
|
507
|
+
raise
|
|
508
|
+
decls = self.__egg_decls__.copy()
|
|
509
|
+
# Special case function application bc we dont support variadic generics yet generally
|
|
510
|
+
if signature == "fn-app":
|
|
511
|
+
fn, *rest_args = args
|
|
512
|
+
args = tuple(rest_args)
|
|
513
|
+
assert not kwargs
|
|
514
|
+
assert isinstance(fn, RuntimeExpr)
|
|
515
|
+
decls.update(fn)
|
|
516
|
+
function_value = fn.__egg_typed_expr__
|
|
517
|
+
fn_tp = function_value.tp
|
|
518
|
+
assert fn_tp.ident == Ident.builtin("UnstableFn")
|
|
519
|
+
fn_return_tp, *fn_arg_tps = fn_tp.args
|
|
520
|
+
signature = FunctionSignature(
|
|
521
|
+
tuple(tp.to_var() for tp in fn_arg_tps),
|
|
522
|
+
tuple(f"_{i}" for i in range(len(fn_arg_tps))),
|
|
523
|
+
(None,) * len(fn_arg_tps),
|
|
524
|
+
fn_return_tp.to_var(),
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
function_value = None
|
|
528
|
+
assert isinstance(signature, FunctionSignature)
|
|
529
|
+
|
|
530
|
+
# Turn all keyword args into positional args
|
|
531
|
+
py_signature = to_py_signature(signature, self.__egg_decls__, _egg_partial_function)
|
|
532
|
+
try:
|
|
533
|
+
bound = py_signature.bind(*args, **kwargs)
|
|
534
|
+
except TypeError as err:
|
|
535
|
+
raise TypeError(f"Failed to bind arguments for {self} with args {args} and kwargs {kwargs}: {err}") from err
|
|
536
|
+
del kwargs
|
|
537
|
+
bound.apply_defaults()
|
|
538
|
+
assert not bound.kwargs
|
|
539
|
+
args = bound.args
|
|
540
|
+
|
|
541
|
+
tcs = TypeConstraintSolver(decls)
|
|
542
|
+
bound_tp = (
|
|
543
|
+
None
|
|
544
|
+
if self.__egg_bound__ is None
|
|
545
|
+
else self.__egg_bound__.__egg_typed_expr__.tp
|
|
546
|
+
if isinstance(self.__egg_bound__, RuntimeExpr)
|
|
547
|
+
else self.__egg_bound__
|
|
548
|
+
)
|
|
549
|
+
if (
|
|
550
|
+
bound_tp
|
|
551
|
+
and bound_tp.args
|
|
552
|
+
# Don't bind class if we have a first class function arg, b/c we don't support that yet
|
|
553
|
+
and not function_value
|
|
554
|
+
):
|
|
555
|
+
tcs.bind_class(bound_tp)
|
|
556
|
+
assert (operator.ge if signature.var_arg_type else operator.eq)(len(args), len(signature.arg_types))
|
|
557
|
+
cls_ident = bound_tp.ident if bound_tp else None
|
|
558
|
+
upcasted_args = [
|
|
559
|
+
resolve_literal(cast("TypeOrVarRef", tp), arg, Thunk.value(decls), tcs=tcs, cls_ident=cls_ident)
|
|
560
|
+
for arg, tp in zip_longest(args, signature.arg_types, fillvalue=signature.var_arg_type)
|
|
561
|
+
]
|
|
562
|
+
decls.update(*upcasted_args)
|
|
563
|
+
arg_exprs = tuple(arg.__egg_typed_expr__ for arg in upcasted_args)
|
|
564
|
+
return_tp = tcs.substitute_typevars(signature.semantic_return_type, cls_ident)
|
|
565
|
+
bound_params = (
|
|
566
|
+
cast("JustTypeRef", bound_tp).args if isinstance(self.__egg_ref__, ClassMethodRef | InitRef) else ()
|
|
567
|
+
)
|
|
568
|
+
# If we were using unstable-app to call a funciton, add that function back as the first arg.
|
|
569
|
+
if function_value:
|
|
570
|
+
arg_exprs = (function_value, *arg_exprs)
|
|
571
|
+
expr_decl = CallDecl(self.__egg_ref__, arg_exprs, bound_params)
|
|
572
|
+
typed_expr_decl = TypedExprDecl(return_tp, expr_decl)
|
|
573
|
+
# If there is not return type, we are mutating the first arg
|
|
574
|
+
if not signature.return_type:
|
|
575
|
+
first_arg = upcasted_args[0]
|
|
576
|
+
first_arg.__egg_decls_thunk__ = Thunk.value(decls)
|
|
577
|
+
first_arg.__egg_typed_expr_thunk__ = Thunk.value(typed_expr_decl)
|
|
578
|
+
return None
|
|
579
|
+
return RuntimeExpr.__from_values__(decls, typed_expr_decl)
|
|
580
|
+
|
|
581
|
+
def __str__(self) -> str:
|
|
582
|
+
first_arg, bound_tp_params = None, None
|
|
583
|
+
match self.__egg_bound__:
|
|
584
|
+
case RuntimeExpr(_):
|
|
585
|
+
first_arg = self.__egg_bound__.__egg_typed_expr__.expr
|
|
586
|
+
case JustTypeRef(_, args):
|
|
587
|
+
bound_tp_params = args
|
|
588
|
+
return pretty_callable_ref(self.__egg_decls__, self.__egg_ref__, first_arg, bound_tp_params)
|
|
589
|
+
|
|
590
|
+
def __repr__(self) -> str:
|
|
591
|
+
return str(self)
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def __doc__(self) -> str | None: # type: ignore[override]
|
|
595
|
+
decl = self.__egg_decls__.get_callable_decl(self.__egg_ref__)
|
|
596
|
+
if isinstance(decl, FunctionDecl | ConstructorDecl):
|
|
597
|
+
return decl.doc
|
|
598
|
+
return None
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def to_py_signature(sig: FunctionSignature, decls: Declarations, optional_args: bool) -> Signature:
|
|
602
|
+
"""
|
|
603
|
+
Convert to a Python signature.
|
|
604
|
+
|
|
605
|
+
If optional_args is true, then all args will be treated as optional, as if a default was provided that makes them
|
|
606
|
+
a var with that arg name as the value.
|
|
607
|
+
|
|
608
|
+
Used for partial application to try binding a function with only some of its args.
|
|
609
|
+
"""
|
|
610
|
+
parameters = [
|
|
611
|
+
Parameter(
|
|
612
|
+
n,
|
|
613
|
+
Parameter.POSITIONAL_OR_KEYWORD,
|
|
614
|
+
default=RuntimeExpr.__from_values__(decls, TypedExprDecl(t.to_just(), d or LetRefDecl(n)))
|
|
615
|
+
if d is not None or optional_args
|
|
616
|
+
else Parameter.empty,
|
|
617
|
+
)
|
|
618
|
+
for n, d, t in zip(sig.arg_names, sig.arg_defaults, sig.arg_types, strict=True)
|
|
619
|
+
]
|
|
620
|
+
if isinstance(sig, FunctionSignature) and sig.var_arg_type is not None:
|
|
621
|
+
parameters.append(Parameter("__rest", Parameter.VAR_POSITIONAL))
|
|
622
|
+
return Signature(parameters)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
ON_CREATE_EXPR: None | Callable[[Callable[[], TypedExprDecl]], None] = None
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@dataclass
|
|
629
|
+
class RuntimeExpr(DelayedDeclerations):
|
|
630
|
+
__egg_typed_expr_thunk__: Callable[[], TypedExprDecl]
|
|
631
|
+
|
|
632
|
+
def __post_init__(self) -> None:
|
|
633
|
+
if ON_CREATE_EXPR:
|
|
634
|
+
ON_CREATE_EXPR(self.__egg_typed_expr_thunk__)
|
|
635
|
+
|
|
636
|
+
@classmethod
|
|
637
|
+
def __from_values__(cls, d: Declarations, e: TypedExprDecl) -> RuntimeExpr:
|
|
638
|
+
return cls(Thunk.value(d), Thunk.value(e))
|
|
639
|
+
|
|
640
|
+
def __with_expr__(self, e: TypedExprDecl) -> RuntimeExpr:
|
|
641
|
+
return RuntimeExpr(self.__egg_decls_thunk__, Thunk.value(e))
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def __egg_typed_expr__(self) -> TypedExprDecl:
|
|
645
|
+
return self.__egg_typed_expr_thunk__()
|
|
646
|
+
|
|
647
|
+
def __getattr__(self, name: str) -> object:
|
|
648
|
+
if (method := _get_expr_method(self, name)) is not _no_method_sentinel:
|
|
649
|
+
return method
|
|
650
|
+
if name in self.__egg_class_decl__.properties:
|
|
651
|
+
fn = RuntimeFunction(
|
|
652
|
+
self.__egg_decls_thunk__, Thunk.value(PropertyRef(self.__egg_class_ident__, name)), self
|
|
653
|
+
)
|
|
654
|
+
return fn()
|
|
655
|
+
if (getattr_method := _get_expr_method(self, "__getattr__")) is not _no_method_sentinel:
|
|
656
|
+
return cast("Callable", getattr_method)(name)
|
|
657
|
+
raise AttributeError(f"{self.__egg_class_ident__} has no method {name}") from None
|
|
658
|
+
|
|
659
|
+
def __repr__(self) -> str:
|
|
660
|
+
"""
|
|
661
|
+
The repr of the expr is the pretty printed version of the expr.
|
|
662
|
+
"""
|
|
663
|
+
if (method := _get_expr_method(self, "__repr__")) is not _no_method_sentinel:
|
|
664
|
+
return cast("str", cast("Any", cast("Callable", method)()))
|
|
665
|
+
return str(self)
|
|
666
|
+
|
|
667
|
+
def __str__(self) -> str:
|
|
668
|
+
if (method := _get_expr_method(self, "__str__")) is not _no_method_sentinel:
|
|
669
|
+
return cast("str", cast("Any", cast("Callable", method)()))
|
|
670
|
+
return self.__egg_pretty__(None)
|
|
671
|
+
|
|
672
|
+
def __egg_pretty__(self, wrapping_fn: str | None) -> str:
|
|
673
|
+
return pretty_decl(self.__egg_decls__, self.__egg_typed_expr__.expr, wrapping_fn=wrapping_fn)
|
|
674
|
+
|
|
675
|
+
def _ipython_display_(self) -> None:
|
|
676
|
+
from IPython.display import Code, display # noqa: PLC0415
|
|
677
|
+
|
|
678
|
+
display(Code(str(self), language="python"))
|
|
679
|
+
|
|
680
|
+
def __dir__(self) -> Iterable[str]:
|
|
681
|
+
cls_decl = self.__egg_class_decl__
|
|
682
|
+
return (
|
|
683
|
+
list(cls_decl.class_methods)
|
|
684
|
+
+ list(cls_decl.class_variables)
|
|
685
|
+
+ list(cls_decl.preserved_methods)
|
|
686
|
+
+ list(cls_decl.methods)
|
|
687
|
+
+ list(cls_decl.properties)
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
@property
|
|
691
|
+
def __egg_class_ident__(self) -> Ident:
|
|
692
|
+
return self.__egg_typed_expr__.tp.ident
|
|
693
|
+
|
|
694
|
+
@property
|
|
695
|
+
def __egg_class_decl__(self) -> ClassDecl:
|
|
696
|
+
return self.__egg_decls__.get_class_decl(self.__egg_class_ident__)
|
|
697
|
+
|
|
698
|
+
# Implement these so that copy() works on this object
|
|
699
|
+
# otherwise copy will try to call `__getstate__` before object is initialized with properties which will cause inifinite recursion
|
|
700
|
+
|
|
701
|
+
def __getstate__(self) -> tuple[Declarations, TypedExprDecl]:
|
|
702
|
+
return self.__egg_decls__, self.__egg_typed_expr__
|
|
703
|
+
|
|
704
|
+
def __setstate__(self, d: tuple[Declarations, TypedExprDecl]) -> None:
|
|
705
|
+
self.__egg_decls_thunk__ = Thunk.value(d[0])
|
|
706
|
+
self.__egg_typed_expr_thunk__ = Thunk.value(d[1])
|
|
707
|
+
|
|
708
|
+
def __hash__(self) -> int:
|
|
709
|
+
if (method := _get_expr_method(self, "__hash__")) is not _no_method_sentinel:
|
|
710
|
+
return cast("int", cast("Any", cast("Callable", method)()))
|
|
711
|
+
return hash(self.__egg_typed_expr__)
|
|
712
|
+
|
|
713
|
+
# Implement this directly to special case behavior where it transforms to an egraph equality, if it is not a
|
|
714
|
+
# preserved method or defined on the class
|
|
715
|
+
def __eq__(self, other: object) -> object: # type: ignore[override]
|
|
716
|
+
if (method := _get_expr_method(self, "__eq__")) is not _no_method_sentinel:
|
|
717
|
+
return cast("Callable", method)(other)
|
|
718
|
+
|
|
719
|
+
if not (isinstance(self, RuntimeExpr) and isinstance(other, RuntimeExpr)):
|
|
720
|
+
return NotImplemented
|
|
721
|
+
if self.__egg_typed_expr__.tp != other.__egg_typed_expr__.tp:
|
|
722
|
+
return NotImplemented
|
|
723
|
+
|
|
724
|
+
from .egraph import Fact # noqa: PLC0415
|
|
725
|
+
|
|
726
|
+
return Fact(
|
|
727
|
+
Declarations.create(self, other),
|
|
728
|
+
EqDecl(self.__egg_typed_expr__.tp, self.__egg_typed_expr__.expr, other.__egg_typed_expr__.expr),
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def __ne__(self, other: object) -> object: # type: ignore[override]
|
|
732
|
+
if (method := _get_expr_method(self, "__ne__")) is not _no_method_sentinel:
|
|
733
|
+
return cast("Callable", method)(other)
|
|
734
|
+
|
|
735
|
+
from .egraph import BaseExpr, ne # noqa: PLC0415
|
|
736
|
+
|
|
737
|
+
return ne(cast("BaseExpr", self)).to(cast("BaseExpr", other))
|
|
738
|
+
|
|
739
|
+
def __call__(
|
|
740
|
+
self, *args: object, **kwargs: object
|
|
741
|
+
) -> object: # define it here only for type checking, it will be overriden below
|
|
742
|
+
...
|
|
743
|
+
|
|
744
|
+
def __replace_expr__(self, new_expr: RuntimeExpr) -> None:
|
|
745
|
+
self.__egg_decls_thunk__ = new_expr.__egg_decls_thunk__
|
|
746
|
+
self.__egg_typed_expr_thunk__ = new_expr.__egg_typed_expr_thunk__
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
# Return a sentinel value to differentiate between no method and property that returns None
|
|
750
|
+
_no_method_sentinel = object()
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _get_expr_method(expr: RuntimeExpr, name: str) -> object:
|
|
754
|
+
if name in (preserved_methods := expr.__egg_class_decl__.preserved_methods):
|
|
755
|
+
return preserved_methods[name].__get__(expr)
|
|
756
|
+
if name in expr.__egg_class_decl__.methods:
|
|
757
|
+
return RuntimeFunction(expr.__egg_decls_thunk__, Thunk.value(MethodRef(expr.__egg_class_ident__, name)), expr)
|
|
758
|
+
return _no_method_sentinel
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def define_expr_method(name: str) -> None:
|
|
762
|
+
"""
|
|
763
|
+
Given the name of a method, explicitly defines it on the runtime type that holds `Expr` objects as a method.
|
|
764
|
+
|
|
765
|
+
Call this if you need a method to be defined on the type itself where overrding with `__getattr__` does not suffice,
|
|
766
|
+
like for NumPy's `__array_ufunc__`.
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
def _defined_method(self: RuntimeExpr, *args, __name: str = name, **kwargs) -> object:
|
|
770
|
+
fn = cast("Callable", _get_expr_method(self, __name))
|
|
771
|
+
if fn is _no_method_sentinel:
|
|
772
|
+
if __name == "__hash__":
|
|
773
|
+
return hash(self.__egg_typed_expr__)
|
|
774
|
+
raise AttributeError(f"{self.__egg_class_ident__} expression has no method {__name}")
|
|
775
|
+
return fn(*args, **kwargs)
|
|
776
|
+
|
|
777
|
+
setattr(RuntimeExpr, name, _defined_method)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
for name in TYPE_DEFINED_METHODS:
|
|
781
|
+
define_expr_method(name)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
for name, r_method in itertools.product(NUMERIC_BINARY_METHODS, (False, True)):
|
|
785
|
+
method_name = f"__r{name[2:]}" if r_method else name
|
|
786
|
+
|
|
787
|
+
def _numeric_binary_method(
|
|
788
|
+
self: object, other: object, name: str = name, r_method: bool = r_method, method_name: str = method_name
|
|
789
|
+
) -> object:
|
|
790
|
+
"""
|
|
791
|
+
Implements numeric binary operations.
|
|
792
|
+
|
|
793
|
+
Tries to find the minimum cost conversion of either the LHS or the RHS, by finding all methods with either
|
|
794
|
+
the LHS or the RHS as exactly the right type and then upcasting the other to that type.
|
|
795
|
+
"""
|
|
796
|
+
from .conversion import ( # noqa: PLC0415
|
|
797
|
+
ConvertError,
|
|
798
|
+
convert_to_same_type,
|
|
799
|
+
min_binary_conversion,
|
|
800
|
+
resolve_type,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
# 1. switch if reversed method
|
|
804
|
+
if r_method:
|
|
805
|
+
self, other = other, self
|
|
806
|
+
# First check if we have a preserved method for this:
|
|
807
|
+
if isinstance(self, RuntimeExpr) and (
|
|
808
|
+
(preserved_method := self.__egg_class_decl__.preserved_methods.get(name)) is not None
|
|
809
|
+
):
|
|
810
|
+
return preserved_method.__get__(self)(other)
|
|
811
|
+
# Then check if the self is a Python type and the other side has a preserved method
|
|
812
|
+
if (
|
|
813
|
+
not isinstance(self, RuntimeExpr)
|
|
814
|
+
and isinstance(other, RuntimeExpr)
|
|
815
|
+
and ((preserved_method := other.__egg_class_decl__.preserved_methods.get(name)) is not None)
|
|
816
|
+
):
|
|
817
|
+
try:
|
|
818
|
+
new_self = convert_to_same_type(self, other)
|
|
819
|
+
except ConvertError:
|
|
820
|
+
pass
|
|
821
|
+
else:
|
|
822
|
+
return preserved_method.__get__(new_self)(other)
|
|
823
|
+
|
|
824
|
+
# If the types don't exactly match to start, then we need to try converting one of them, by finding the cheapest conversion
|
|
825
|
+
if not (
|
|
826
|
+
isinstance(self, RuntimeExpr)
|
|
827
|
+
and isinstance(other, RuntimeExpr)
|
|
828
|
+
and (
|
|
829
|
+
self.__egg_decls__.check_binary_method_with_types(
|
|
830
|
+
name, self.__egg_typed_expr__.tp, other.__egg_typed_expr__.tp
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
):
|
|
834
|
+
best_method = min_binary_conversion(name, resolve_type(self), resolve_type(other))
|
|
835
|
+
|
|
836
|
+
if not best_method:
|
|
837
|
+
raise RuntimeError(
|
|
838
|
+
f"Cannot resolve {name} for {resolve_type(self)} and {resolve_type(other)}, no conversion found"
|
|
839
|
+
)
|
|
840
|
+
self, other = best_method[0](self), best_method[1](other)
|
|
841
|
+
|
|
842
|
+
method_ref = MethodRef(self.__egg_class_ident__, name)
|
|
843
|
+
fn = RuntimeFunction(Thunk.value(self.__egg_decls__), Thunk.value(method_ref), self)
|
|
844
|
+
return fn(other)
|
|
845
|
+
|
|
846
|
+
setattr(RuntimeExpr, method_name, _numeric_binary_method)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def resolve_callable(callable: object) -> tuple[CallableRef, Declarations]:
|
|
850
|
+
"""
|
|
851
|
+
Resolves a runtime callable into a ref
|
|
852
|
+
"""
|
|
853
|
+
# TODO: Make runtime class work with __match_args__
|
|
854
|
+
if isinstance(callable, RuntimeClass):
|
|
855
|
+
return InitRef(callable.__egg_tp__.ident), callable.__egg_decls__
|
|
856
|
+
match callable:
|
|
857
|
+
case RuntimeFunction(decls, ref, _):
|
|
858
|
+
return ref(), decls()
|
|
859
|
+
case RuntimeExpr(decl_thunk, expr_thunk):
|
|
860
|
+
if not isinstance((expr := expr_thunk().expr), CallDecl) or not isinstance(
|
|
861
|
+
expr.callable, ConstantRef | ClassVariableRef
|
|
862
|
+
):
|
|
863
|
+
raise NotImplementedError(f"Can only turn constants or classvars into callable refs, not {expr}")
|
|
864
|
+
return expr.callable, decl_thunk()
|
|
865
|
+
case types.MethodWrapperType() if isinstance((slf := callable.__self__), RuntimeClass):
|
|
866
|
+
return MethodRef(slf.__egg_tp__.ident, callable.__name__), slf.__egg_decls__
|
|
867
|
+
case _:
|
|
868
|
+
raise NotImplementedError(f"Cannot turn {callable} of type {type(callable)} into a callable ref")
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def create_callable(decls: Declarations, ref: CallableRef) -> RuntimeClass | RuntimeFunction | RuntimeExpr:
|
|
872
|
+
"""
|
|
873
|
+
Creates a callable object from a callable ref. This might not actually be callable, if the ref is a constant
|
|
874
|
+
or classvar then it is a value
|
|
875
|
+
"""
|
|
876
|
+
match ref:
|
|
877
|
+
case InitRef(name):
|
|
878
|
+
return RuntimeClass(Thunk.value(decls), TypeRefWithVars(name))
|
|
879
|
+
case FunctionRef() | MethodRef() | ClassMethodRef() | PropertyRef() | UnnamedFunctionRef():
|
|
880
|
+
bound = JustTypeRef(ref.ident) if isinstance(ref, ClassMethodRef) else None
|
|
881
|
+
return RuntimeFunction(Thunk.value(decls), Thunk.value(ref), bound)
|
|
882
|
+
case ConstantRef(name):
|
|
883
|
+
tp = decls._constants[name].type_ref
|
|
884
|
+
case ClassVariableRef(cls_name, var_name):
|
|
885
|
+
tp = decls._classes[cls_name].class_variables[var_name].type_ref
|
|
886
|
+
case _:
|
|
887
|
+
assert_never(ref)
|
|
888
|
+
return RuntimeExpr.__from_values__(decls, TypedExprDecl(tp, CallDecl(ref)))
|