egglog 9.0.0__cp313-cp313-win_amd64.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.

Potentially problematic release.


This version of egglog might be problematic. Click here for more details.

@@ -0,0 +1,424 @@
1
+ # mypy: disable-error-code="empty-body"
2
+ """
3
+ Builds up imperative string expressions from a functional expression.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TypeAlias, Union
9
+
10
+ from egglog import *
11
+
12
+ ProgramLike: TypeAlias = Union["Program", StringLike]
13
+
14
+
15
+ class Program(Expr):
16
+ """
17
+ Semanticallly represents an expression with a number of ordered statements that it depends on to run.
18
+
19
+ The expression and statements are all represented as strings.
20
+ """
21
+
22
+ def __init__(self, expr: StringLike, is_identifier: BoolLike = Bool(False)) -> None:
23
+ """
24
+ Create a program based on a string expression.
25
+ """
26
+
27
+ def __add__(self, other: ProgramLike) -> Program:
28
+ """
29
+ Concats the strings of the two expressions and also the statements.
30
+ """
31
+
32
+ @method(unextractable=True)
33
+ def statement(self, statement: ProgramLike) -> Program:
34
+ """
35
+ Uses the expression of the statement and adds that as a statement to the program.
36
+ """
37
+
38
+ def assign(self) -> Program:
39
+ """
40
+ Returns a new program with the expression assigned to a gensym.
41
+ """
42
+
43
+ def function_two(self, arg1: ProgramLike, arg2: ProgramLike, name: StringLike = String("__fn")) -> Program:
44
+ """
45
+ Returns a new program defining a function with two arguments.
46
+ """
47
+
48
+ def function_three(
49
+ self, arg1: ProgramLike, arg2: ProgramLike, arg3: ProgramLike, name: StringLike = String("__fn")
50
+ ) -> Program:
51
+ """
52
+ Returns a new program defining a function with three arguments.
53
+ """
54
+
55
+ def expr_to_statement(self) -> Program:
56
+ """
57
+ Returns a new program with the expression as a statement and the new expression empty.
58
+ """
59
+
60
+ @property
61
+ def expr(self) -> String:
62
+ """
63
+ Returns the expression of the program, if it's been compiled
64
+ """
65
+
66
+ @property
67
+ def statements(self) -> String:
68
+ """
69
+ Returns the statements of the program, if it's been compiled
70
+ """
71
+
72
+ @property
73
+ def next_sym(self) -> i64:
74
+ """
75
+ Returns the next gensym to use. This is set after calling `compile(i)` on a program.
76
+ """
77
+
78
+ # TODO: Replace w/ def next_sym(self) -> i64: ... ?
79
+ def compile(self, next_sym: i64 = i64(0)) -> Unit:
80
+ """
81
+ Triggers compilation of the program.
82
+ """
83
+
84
+ @method(merge=lambda old, _new: old) # type: ignore[misc]
85
+ @property
86
+ def parent(self) -> Program:
87
+ """
88
+ Returns the parent of the program, if it's been compiled into the parent.
89
+
90
+ Only keeps the original parent, not any additional ones, so that each set of statements is only added once.
91
+ """
92
+
93
+ @property
94
+ def is_identifer(self) -> Bool:
95
+ """
96
+ Returns whether the expression is an identifier. Used so that we don't re-assign any identifiers.
97
+ """
98
+
99
+
100
+ converter(String, Program, Program)
101
+
102
+
103
+ class EvalProgram(Expr):
104
+ def __init__(self, program: Program, globals: object) -> None:
105
+ """
106
+ Evaluates the program and saves as the py_object
107
+ """
108
+
109
+ # Only allow it to be set once, b/c hash of functions not stable
110
+ @method(merge=lambda old, _new: old) # type: ignore[misc]
111
+ @property
112
+ def as_py_object(self) -> PyObject:
113
+ """
114
+ Returns the python object of the program, if it's been evaluated.
115
+ """
116
+
117
+
118
+ @ruleset
119
+ def eval_program_rulseset(ep: EvalProgram, p: Program, expr: String, statements: String, g: PyObject):
120
+ # When we evaluate a program, we first want to compile to a string
121
+ yield rule(EvalProgram(p, g)).then(p.compile())
122
+ # Then we want to evaluate the statements/expr
123
+ yield rule(
124
+ eq(ep).to(EvalProgram(p, g)),
125
+ eq(p.statements).to(statements),
126
+ eq(p.expr).to(expr),
127
+ ).then(
128
+ set_(ep.as_py_object).to(
129
+ py_eval(
130
+ "l['___res']",
131
+ PyObject.dict(PyObject.from_string("l"), py_exec(join(statements, "\n", "___res = ", expr), g)),
132
+ )
133
+ ),
134
+ )
135
+
136
+
137
+ @ruleset
138
+ def program_gen_ruleset(
139
+ s: String,
140
+ s1: String,
141
+ s2: String,
142
+ s3: String,
143
+ s4: String,
144
+ s5: String,
145
+ s6: String,
146
+ p: Program,
147
+ p1: Program,
148
+ p2: Program,
149
+ p3: Program,
150
+ p4: Program,
151
+ i: i64,
152
+ i2: i64,
153
+ b: Bool,
154
+ ):
155
+ ##
156
+ # Expression
157
+ ##
158
+
159
+ # Compiling a string just gives that string
160
+ yield rule(eq(p).to(Program(s, b)), p.compile(i)).then(
161
+ set_(p.expr).to(s),
162
+ set_(p.statements).to(String("")),
163
+ set_(p.next_sym).to(i),
164
+ set_(p.is_identifer).to(b),
165
+ )
166
+
167
+ ##
168
+ # Statement
169
+ ##
170
+ # Compiling a statement means that we should use the expression of the statement as a statement and use the expression of the first
171
+ yield rewrite(p1.statement(p2)).to(p1 + p2.expr_to_statement())
172
+
173
+ ##
174
+ # Expr to statement
175
+ ##
176
+ stmt = eq(p).to(p1.expr_to_statement())
177
+ # 1. Set parent and is_identifier to false, since its empty
178
+ yield rule(stmt, p.compile(i)).then(set_(p1.parent).to(p), set_(p.is_identifer).to(Bool(False)))
179
+ # 2. Compile p1 if parent set
180
+ yield rule(stmt, p.compile(i), eq(p1.parent).to(p)).then(p1.compile(i))
181
+ # 3.a. If parent not set, set statements to expr
182
+ yield rule(
183
+ stmt,
184
+ p.compile(i),
185
+ ne(p1.parent).to(p),
186
+ eq(s1).to(p1.expr),
187
+ ).then(
188
+ set_(p.statements).to(join(s1, "\n")),
189
+ set_(p.next_sym).to(i),
190
+ set_(p.expr).to(String("")),
191
+ )
192
+ # 3.b. If parent set, set statements to expr + statements
193
+ yield rule(
194
+ stmt,
195
+ eq(p1.parent).to(p),
196
+ eq(s1).to(p1.expr),
197
+ eq(s2).to(p1.statements),
198
+ eq(i).to(p1.next_sym),
199
+ ).then(
200
+ set_(p.statements).to(join(s2, s1, "\n")),
201
+ set_(p.next_sym).to(i),
202
+ set_(p.expr).to(String("")),
203
+ )
204
+
205
+ ##
206
+ # Addition
207
+ ##
208
+
209
+ # Compiling an addition is the same as compiling one, then the other, then setting the expression as the addition
210
+ # of the two
211
+ program_add = eq(p).to(p1 + p2)
212
+
213
+ # If the resulting expression is either of the inputs, then its an identifer if those are
214
+ # Otherwise, if its not equal to either input, its not an identifier
215
+ yield rule(program_add, eq(p.expr).to(p1.expr), eq(b).to(p1.is_identifer)).then(set_(p.is_identifer).to(b))
216
+ yield rule(program_add, eq(p.expr).to(p2.expr), eq(b).to(p2.is_identifer)).then(set_(p.is_identifer).to(b))
217
+ yield rule(program_add, ne(p.expr).to(p1.expr), ne(p.expr).to(p2.expr)).then(set_(p.is_identifer).to(Bool(False)))
218
+
219
+ # Set parent of p1
220
+ yield rule(program_add, p.compile(i)).then(
221
+ set_(p1.parent).to(p),
222
+ )
223
+
224
+ # Compile p1, if p1 parent equal
225
+ yield rule(program_add, p.compile(i), eq(p1.parent).to(p)).then(p1.compile(i))
226
+
227
+ # Set parent of p2, once p1 compiled
228
+ yield rule(program_add, p.compile(i), p1.next_sym).then(set_(p2.parent).to(p))
229
+
230
+ # Compile p2, if p1 parent not equal, but p2 parent equal
231
+ yield rule(program_add, p.compile(i), ne(p1.parent).to(p), eq(p2.parent).to(p)).then(p2.compile(i))
232
+
233
+ # Compile p2, if p1 parent eqal
234
+ yield rule(program_add, p.compile(i2), eq(p1.parent).to(p), eq(i).to(p1.next_sym), eq(p2.parent).to(p)).then(
235
+ p2.compile(i)
236
+ )
237
+
238
+ # Set p expr to join of p1 and p2
239
+ yield rule(
240
+ program_add,
241
+ eq(s1).to(p1.expr),
242
+ eq(s2).to(p2.expr),
243
+ ).then(
244
+ set_(p.expr).to(join(s1, s2)),
245
+ )
246
+ # Set p statements to join and next sym to p2 if both parents set
247
+ yield rule(
248
+ program_add,
249
+ eq(p1.parent).to(p),
250
+ eq(p2.parent).to(p),
251
+ eq(s1).to(p1.statements),
252
+ eq(s2).to(p2.statements),
253
+ eq(i).to(p2.next_sym),
254
+ ).then(
255
+ set_(p.statements).to(join(s1, s2)),
256
+ set_(p.next_sym).to(i),
257
+ )
258
+ # Set p statements to empty and next sym to i if neither parents set
259
+ yield rule(
260
+ program_add,
261
+ p.compile(i),
262
+ ne(p1.parent).to(p),
263
+ ne(p2.parent).to(p),
264
+ ).then(
265
+ set_(p.statements).to(String("")),
266
+ set_(p.next_sym).to(i),
267
+ )
268
+ # Set p statements to p1 and next sym to p1 if p1 parent set and p2 parent not set
269
+ yield rule(
270
+ program_add,
271
+ eq(p1.parent).to(p),
272
+ ne(p2.parent).to(p),
273
+ eq(s1).to(p1.statements),
274
+ eq(i).to(p1.next_sym),
275
+ ).then(
276
+ set_(p.statements).to(s1),
277
+ set_(p.next_sym).to(i),
278
+ )
279
+ # Set p statements to p2 and next sym to p2 if p2 parent set and p1 parent not set
280
+ yield rule(
281
+ program_add,
282
+ eq(p2.parent).to(p),
283
+ ne(p1.parent).to(p),
284
+ eq(s2).to(p2.statements),
285
+ eq(i).to(p2.next_sym),
286
+ ).then(
287
+ set_(p.statements).to(s2),
288
+ set_(p.next_sym).to(i),
289
+ )
290
+
291
+ ##
292
+ # Assign
293
+ ##
294
+
295
+ # Compiling an assign is the same as compiling the expression, adding an assign statement, then setting the
296
+ # expression as the gensym, and setting is_identifier to true
297
+ program_assign = eq(p).to(p1.assign())
298
+ # Set parent
299
+ yield rule(program_assign, p.compile(i)).then(set_(p1.parent).to(p), set_(p.is_identifer).to(Bool(True)))
300
+ # If parent set, compile the expression
301
+ yield rule(program_assign, p.compile(i), eq(p1.parent).to(p)).then(p1.compile(i))
302
+
303
+ # 1. If p1 is not an identifier, then we must create a new one
304
+
305
+ # 1. a. If p1 parent is p, then use statements of p, next sym of p
306
+ symbol = join(String("_"), i.to_string())
307
+ yield rule(
308
+ program_assign,
309
+ eq(p1.parent).to(p),
310
+ eq(s1).to(p1.statements),
311
+ eq(i).to(p1.next_sym),
312
+ eq(s2).to(p1.expr),
313
+ eq(p1.is_identifer).to(Bool(False)),
314
+ ).then(
315
+ set_(p.statements).to(join(s1, symbol, " = ", s2, "\n")),
316
+ set_(p.expr).to(symbol),
317
+ set_(p.next_sym).to(i + 1),
318
+ )
319
+ # 1. b. If p1 parent is not p, then just use assign as statement, next sym of i
320
+ yield rule(
321
+ program_assign,
322
+ ne(p1.parent).to(p),
323
+ p.compile(i),
324
+ eq(s2).to(p1.expr),
325
+ eq(p1.is_identifer).to(Bool(False)),
326
+ ).then(
327
+ set_(p.statements).to(join(symbol, " = ", s2, "\n")),
328
+ set_(p.expr).to(symbol),
329
+ set_(p.next_sym).to(i + 1),
330
+ )
331
+
332
+ # 2. If p1 is an identifier, then program assign is a no-op
333
+
334
+ # 1. a. If p1 parent is p, then use statements of p, next sym of p
335
+ yield rule(
336
+ program_assign,
337
+ eq(p1.parent).to(p),
338
+ eq(s1).to(p1.statements),
339
+ eq(i).to(p1.next_sym),
340
+ eq(s2).to(p1.expr),
341
+ eq(p1.is_identifer).to(Bool(True)),
342
+ ).then(
343
+ set_(p.statements).to(s1),
344
+ set_(p.expr).to(s2),
345
+ set_(p.next_sym).to(i),
346
+ )
347
+ # 1. b. If p1 parent is not p, then just use assign as statement, next sym of i
348
+ yield rule(
349
+ program_assign,
350
+ ne(p1.parent).to(p),
351
+ p.compile(i),
352
+ eq(s2).to(p1.expr),
353
+ eq(p1.is_identifer).to(Bool(True)),
354
+ ).then(
355
+ set_(p.statements).to(String("")),
356
+ set_(p.expr).to(s2),
357
+ set_(p.next_sym).to(i),
358
+ )
359
+
360
+ ##
361
+ # Function two
362
+ ##
363
+
364
+ # When compiling a function, the two args, p2 and p3, should get compiled when we compile p1, and should just be vars.
365
+ fn_two = eq(p).to(p1.function_two(p2, p3, s1))
366
+ # 1. Set parents of both args to p and compile them
367
+ # Assumes that this if the first thing to compile, so no need to check, and assumes that compiling args doesn't result in any
368
+ # change in the next sym
369
+ yield rule(fn_two, p.compile(i)).then(
370
+ set_(p2.parent).to(p),
371
+ set_(p3.parent).to(p),
372
+ set_(p1.parent).to(p),
373
+ p2.compile(i),
374
+ p3.compile(i),
375
+ p1.compile(i),
376
+ set_(p.is_identifer).to(Bool(True)),
377
+ )
378
+ # 2. Set statements to function body and the next sym to i
379
+ yield rule(
380
+ fn_two,
381
+ p.compile(i),
382
+ eq(s2).to(p1.expr),
383
+ eq(s3).to(p1.statements),
384
+ eq(s4).to(p2.expr),
385
+ eq(s5).to(p3.expr),
386
+ ).then(
387
+ set_(p.statements).to(
388
+ join("def ", s1, "(", s4, ", ", s5, "):\n ", s3.replace("\n", "\n "), "return ", s2, "\n")
389
+ ),
390
+ set_(p.next_sym).to(i),
391
+ set_(p.expr).to(s1),
392
+ )
393
+
394
+ ##
395
+ # Function three
396
+ ##
397
+
398
+ fn_three = eq(p).to(p1.function_three(p2, p3, p4, s1))
399
+ yield rule(fn_three, p.compile(i)).then(
400
+ set_(p2.parent).to(p),
401
+ set_(p3.parent).to(p),
402
+ set_(p1.parent).to(p),
403
+ set_(p4.parent).to(p),
404
+ p2.compile(i),
405
+ p3.compile(i),
406
+ p1.compile(i),
407
+ p4.compile(i),
408
+ set_(p.is_identifer).to(Bool(True)),
409
+ )
410
+ yield rule(
411
+ fn_three,
412
+ p.compile(i),
413
+ eq(s2).to(p1.expr),
414
+ eq(s3).to(p1.statements),
415
+ eq(s4).to(p2.expr),
416
+ eq(s5).to(p3.expr),
417
+ eq(s6).to(p4.expr),
418
+ ).then(
419
+ set_(p.statements).to(
420
+ join("def ", s1, "(", s4, ", ", s5, ", ", s6, "):\n ", s3.replace("\n", "\n "), "return ", s2, "\n")
421
+ ),
422
+ set_(p.next_sym).to(i),
423
+ set_(p.expr).to(s1),
424
+ )
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ # https://github.com/sklam/pyasir/blob/c363ff4f8f91177700ad4108dd5042b9b97d8289/pyasir/tests/test_fib.py
5
+
6
+ # In progress - should be able to re-create this
7
+ # @df.func
8
+ # def fib_ir(n: pyasir.Int64) -> pyasir.Int64:
9
+ # @df.switch(n <= 1)
10
+ # def swt(n):
11
+ # @df.case(1)
12
+ # def case0(n):
13
+ # return 1
14
+
15
+ # @df.case(0)
16
+ # def case1(n):
17
+ # return fib_ir(n - 1) + fib_ir(n - 2)
18
+
19
+ # yield case0
20
+ # yield case1
21
+
22
+ # r = swt(n)
23
+ # return r
24
+
25
+
26
+ # With something like this:
27
+ # @egglog.function
28
+ # def fib(n: Int) -> Int:
29
+ # return (n <= Int(1)).if_int(
30
+ # Int(1),
31
+ # fib(n - Int(1)) + fib(n - Int(2)),
32
+ # )
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from functools import partial
5
+ from inspect import Parameter, signature
6
+ from typing import Any, TypeVar, cast
7
+
8
+ __all__ = ["functionalize"]
9
+
10
+
11
+ T = TypeVar("T", bound=Callable)
12
+
13
+
14
+ # TODO: Add `to_lift` param so that we only transform those with vars in them to args
15
+
16
+
17
+ def functionalize(f: T, get_annotation: Callable[[object], type | None]) -> T:
18
+ """
19
+ Takes a function and returns a new function with all names (co_names) and free variables (co_freevars) added as arguments
20
+ and then partially applied with their values. The second arg, get_annotation, will be applied to all values
21
+ to get their type annotation. If it is None, that arg will not be added as a parameter.
22
+
23
+ For example if you have:
24
+
25
+ def get_annotation(x): return int if x <= 10 else None
26
+
27
+ g = 10
28
+ def f(a, a2):
29
+ def h(b: Z):
30
+ return a + a2 + b + g
31
+
32
+ return functionalize(h, get_annotation)
33
+ res = f(9, 11)
34
+
35
+ It should be equivalent to (according to body, signature, and annotations) (Note that the new arguments will be positional only):
36
+
37
+ def h(a: get_annotation(a), g: get_annotation(g), b: Z):
38
+ return a + b + g
39
+ res = partial(h, a, g)
40
+ """
41
+ code = f.__code__
42
+ names = tuple(n for n in code.co_names if n in f.__globals__)
43
+ free_vars = code.co_freevars
44
+
45
+ global_values: list[Any] = [f.__globals__[name] for name in names]
46
+ free_var_values = [cell.cell_contents for cell in f.__closure__] if f.__closure__ else []
47
+ assert len(free_var_values) == len(free_vars), "Free vars and their values do not match"
48
+ global_values_filtered = [
49
+ (i, name, value, annotation)
50
+ for i, (name, value) in enumerate(zip(names, global_values, strict=True))
51
+ if (annotation := get_annotation(value)) is not None
52
+ ]
53
+ free_var_values_filtered = [
54
+ (i, name, value, annotation)
55
+ for i, (name, value) in enumerate(zip(free_vars, free_var_values, strict=True))
56
+ if (annotation := get_annotation(value)) is not None
57
+ ]
58
+ additional_arg_filtered = global_values_filtered + free_var_values_filtered
59
+
60
+ # Create a wrapper function
61
+ def wrapper(*args):
62
+ # Split args into names, free vars and other args
63
+ name_args, free_var_args, rest_args = (
64
+ args[: (n_names := len(global_values_filtered))],
65
+ args[n_names : (n_args := len(additional_arg_filtered))],
66
+ args[n_args:],
67
+ )
68
+ # Update globals with names
69
+ f.__globals__.update({
70
+ name: arg for (_, name, _, _), arg in zip(global_values_filtered, name_args, strict=False)
71
+ })
72
+ # update function free vars with free var args
73
+ for (i, _, _, _), value in zip(free_var_values_filtered, free_var_args, strict=True):
74
+ assert f.__closure__, "Function does not have closure"
75
+ f.__closure__[i].cell_contents = value
76
+ return f(*rest_args)
77
+
78
+ # Set the signature of the new function to a signature with the free vars and names added as arguments
79
+ orig_signature = signature(f)
80
+ wrapper.__signature__ = orig_signature.replace( # type: ignore[attr-defined]
81
+ parameters=[
82
+ *[Parameter(n, Parameter.POSITIONAL_OR_KEYWORD) for _, n, _, _ in additional_arg_filtered],
83
+ *orig_signature.parameters.values(),
84
+ ]
85
+ )
86
+ # Set the annotations of the new function to the annotations of the original function + annotations of passed in values
87
+ wrapper.__annotations__ = f.__annotations__ | {n: a for _, n, _, a in additional_arg_filtered}
88
+ wrapper.__name__ = f.__name__
89
+
90
+ # Partially apply the wrapper function with the current values of the free vars
91
+ return cast("T", partial(wrapper, *(v for _, _, v, _ in additional_arg_filtered)))
@@ -0,0 +1,41 @@
1
+ from .bindings import EGraph
2
+
3
+ EGRAPH_VAR = "_MAGIC_EGRAPH"
4
+
5
+ try:
6
+ get_ipython() # type: ignore[name-defined]
7
+ IN_IPYTHON = True
8
+ except NameError:
9
+ IN_IPYTHON = False
10
+
11
+ if IN_IPYTHON:
12
+ import graphviz
13
+ from IPython.core.magic import needs_local_scope, register_cell_magic
14
+
15
+ @needs_local_scope
16
+ @register_cell_magic
17
+ def egglog(line, cell, local_ns):
18
+ """
19
+ Run an egglog program.
20
+
21
+ Usage:
22
+
23
+ %%egglog [output] [continue] [graph]
24
+ (egglog program)
25
+
26
+ If `output` is specified, the output of the program will be printed.
27
+ If `continue` is specified, the program will be run in the same EGraph as the previous cell.
28
+ If `graph` is specified, the EGraph will be displayed as a graph.
29
+ """
30
+ if EGRAPH_VAR in local_ns and "continue" in line:
31
+ e = local_ns[EGRAPH_VAR]
32
+ else:
33
+ e = EGraph()
34
+ local_ns[EGRAPH_VAR] = e
35
+ cmds = e.parse_program(cell)
36
+ res = e.run_program(*cmds)
37
+ if "output" in line:
38
+ print("\n".join(res))
39
+ if "graph" in line:
40
+ return graphviz.Source(e.to_graphviz_string())
41
+ return None