lean-python 0.1.0__py3-none-any.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.
- lean_python/__init__.py +6 -0
- lean_python/cli.py +50 -0
- lean_python/embed.py +131 -0
- lean_python/io.py +15 -0
- lean_python/io_externs.py +1192 -0
- lean_python/marshal.py +727 -0
- lean_python/models.py +297 -0
- lean_python/runtime.py +1400 -0
- lean_python/sync.py +265 -0
- lean_python/types.py +33 -0
- lean_python-0.1.0.dist-info/METADATA +231 -0
- lean_python-0.1.0.dist-info/RECORD +15 -0
- lean_python-0.1.0.dist-info/WHEEL +4 -0
- lean_python-0.1.0.dist-info/entry_points.txt +2 -0
- lean_python-0.1.0.dist-info/licenses/LICENSE +202 -0
lean_python/marshal.py
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
"""Conversion between Python values and the runtime's Lean values, and the pydantic types of Lean types.
|
|
2
|
+
|
|
3
|
+
Generated modules describe each public type with a descriptor (``NAT``, ``ListD(...)``, ``NamedD``)
|
|
4
|
+
and each structure and constructor with a ``CtorInfo``. Values going to Lean are checked against
|
|
5
|
+
the Lean type (ranges, invariants) while they are converted; values coming back are built with
|
|
6
|
+
``model_construct``, since a Lean value already satisfies its invariants.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import enum
|
|
10
|
+
import json
|
|
11
|
+
from typing import Annotated, Any, ClassVar
|
|
12
|
+
|
|
13
|
+
import pydantic
|
|
14
|
+
from pydantic import AfterValidator, BaseModel, ConfigDict, GetCoreSchemaHandler, GetJsonSchemaHandler
|
|
15
|
+
from pydantic_core import PydanticCustomError, SchemaSerializer, SchemaValidator, ValidationError, core_schema
|
|
16
|
+
|
|
17
|
+
from lean_python.runtime import Ctor, LArray, _f32, _list_to_py, _py_to_list
|
|
18
|
+
|
|
19
|
+
Field = pydantic.Field
|
|
20
|
+
"""Used by generated models for the field options (``alias``, ``exclude_if``)."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LeanIOError(OSError):
|
|
24
|
+
"""An `IO.Error` raised by a Lean `IO` action."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Fail(Exception):
|
|
28
|
+
def __init__(self, msg, value, loc=()):
|
|
29
|
+
super().__init__(msg)
|
|
30
|
+
self.msg = msg
|
|
31
|
+
self.value = value
|
|
32
|
+
self.loc = loc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fail(msg, value):
|
|
36
|
+
raise _Fail(msg, value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _validation_error(title, f):
|
|
40
|
+
return ValidationError.from_exception_data(
|
|
41
|
+
title,
|
|
42
|
+
[{"type": PydanticCustomError("lean_type", f.msg), "loc": f.loc, "input": f.value}],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Descriptors
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Desc:
|
|
51
|
+
def to_lean(self, v):
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def to_py(self, x):
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_int(v):
|
|
59
|
+
return isinstance(v, int) and not isinstance(v, bool)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _Nat(Desc):
|
|
63
|
+
def to_lean(self, v):
|
|
64
|
+
if not _is_int(v):
|
|
65
|
+
_fail("expected an int (Lean `Nat`)", v)
|
|
66
|
+
if v < 0:
|
|
67
|
+
_fail("expected a non-negative int (Lean `Nat`)", v)
|
|
68
|
+
return v
|
|
69
|
+
|
|
70
|
+
def to_py(self, x):
|
|
71
|
+
return x
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _Int(Desc):
|
|
75
|
+
def to_lean(self, v):
|
|
76
|
+
if not _is_int(v):
|
|
77
|
+
_fail("expected an int (Lean `Int`)", v)
|
|
78
|
+
return v
|
|
79
|
+
|
|
80
|
+
def to_py(self, x):
|
|
81
|
+
return x
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class UIntD(Desc):
|
|
85
|
+
def __init__(self, bits):
|
|
86
|
+
self.bits = bits
|
|
87
|
+
self.limit = 1 << bits
|
|
88
|
+
|
|
89
|
+
def to_lean(self, v):
|
|
90
|
+
if not _is_int(v) or not 0 <= v < self.limit:
|
|
91
|
+
_fail(f"expected an int in [0, 2^{self.bits}) (Lean `UInt{self.bits}`)", v)
|
|
92
|
+
return v
|
|
93
|
+
|
|
94
|
+
def to_py(self, x):
|
|
95
|
+
return x
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SIntD(Desc):
|
|
99
|
+
def __init__(self, bits):
|
|
100
|
+
self.bits = bits
|
|
101
|
+
self.half = 1 << (bits - 1)
|
|
102
|
+
self.mask = (1 << bits) - 1
|
|
103
|
+
|
|
104
|
+
def to_lean(self, v):
|
|
105
|
+
if not _is_int(v) or not -self.half <= v < self.half:
|
|
106
|
+
_fail(f"expected an int in [-2^{self.bits - 1}, 2^{self.bits - 1}) (Lean `Int{self.bits}`)", v)
|
|
107
|
+
return v & self.mask
|
|
108
|
+
|
|
109
|
+
def to_py(self, x):
|
|
110
|
+
return x - (1 << self.bits) if x >= self.half else x
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class FinD(Desc):
|
|
114
|
+
def __init__(self, n):
|
|
115
|
+
self.n = n
|
|
116
|
+
|
|
117
|
+
def to_lean(self, v):
|
|
118
|
+
if not _is_int(v) or not 0 <= v < self.n:
|
|
119
|
+
_fail(f"expected an int in [0, {self.n}) (Lean `Fin {self.n}`)", v)
|
|
120
|
+
return v
|
|
121
|
+
|
|
122
|
+
def to_py(self, x):
|
|
123
|
+
return x
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _Bool(Desc):
|
|
127
|
+
def to_lean(self, v):
|
|
128
|
+
if not isinstance(v, bool):
|
|
129
|
+
_fail("expected a bool", v)
|
|
130
|
+
return 1 if v else 0
|
|
131
|
+
|
|
132
|
+
def to_py(self, x):
|
|
133
|
+
return bool(x)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class _Char(Desc):
|
|
137
|
+
def to_lean(self, v):
|
|
138
|
+
if not isinstance(v, str) or len(v) != 1:
|
|
139
|
+
_fail("expected a str of one character (Lean `Char`)", v)
|
|
140
|
+
c = ord(v)
|
|
141
|
+
if 0xD800 <= c <= 0xDFFF:
|
|
142
|
+
_fail("a surrogate is not a Lean `Char`", v)
|
|
143
|
+
return c
|
|
144
|
+
|
|
145
|
+
def to_py(self, x):
|
|
146
|
+
return chr(x)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _String(Desc):
|
|
150
|
+
def to_lean(self, v):
|
|
151
|
+
if not isinstance(v, str):
|
|
152
|
+
_fail("expected a str", v)
|
|
153
|
+
try:
|
|
154
|
+
return v.encode("utf-8")
|
|
155
|
+
except UnicodeEncodeError:
|
|
156
|
+
_fail("a str with a surrogate is not a Lean `String`", v)
|
|
157
|
+
|
|
158
|
+
def to_py(self, x):
|
|
159
|
+
return x.decode("utf-8")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _Float(Desc):
|
|
163
|
+
def to_lean(self, v):
|
|
164
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
165
|
+
_fail("expected a float", v)
|
|
166
|
+
return float(v)
|
|
167
|
+
|
|
168
|
+
def to_py(self, x):
|
|
169
|
+
return x
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _Float32(_Float):
|
|
173
|
+
def to_lean(self, v):
|
|
174
|
+
return _f32(super().to_lean(v))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _Unit(Desc):
|
|
178
|
+
def to_lean(self, v):
|
|
179
|
+
if v is not None:
|
|
180
|
+
_fail("expected None (Lean `Unit`)", v)
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
def to_py(self, x):
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _at(i, f):
|
|
188
|
+
f.loc = (i, *f.loc)
|
|
189
|
+
return f
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ListD(Desc):
|
|
193
|
+
def __init__(self, d):
|
|
194
|
+
self.d = d
|
|
195
|
+
|
|
196
|
+
def _items(self, v):
|
|
197
|
+
if not isinstance(v, (list, tuple)):
|
|
198
|
+
_fail("expected a list", v)
|
|
199
|
+
out = []
|
|
200
|
+
for i, x in enumerate(v):
|
|
201
|
+
try:
|
|
202
|
+
out.append(self.d.to_lean(x))
|
|
203
|
+
except _Fail as f:
|
|
204
|
+
raise _at(i, f) from None
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
def to_lean(self, v):
|
|
208
|
+
return _py_to_list(self._items(v))
|
|
209
|
+
|
|
210
|
+
def to_py(self, x):
|
|
211
|
+
return [self.d.to_py(e) for e in _list_to_py(x)]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ArrayD(ListD):
|
|
215
|
+
def to_lean(self, v):
|
|
216
|
+
return LArray(self._items(v))
|
|
217
|
+
|
|
218
|
+
def to_py(self, x):
|
|
219
|
+
return [self.d.to_py(e) for e in x.data]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class OptionD(Desc):
|
|
223
|
+
def __init__(self, d):
|
|
224
|
+
self.d = d
|
|
225
|
+
|
|
226
|
+
def to_lean(self, v):
|
|
227
|
+
if v is None:
|
|
228
|
+
return 0
|
|
229
|
+
return Ctor(1, [self.d.to_lean(v)])
|
|
230
|
+
|
|
231
|
+
def to_py(self, x):
|
|
232
|
+
if isinstance(x, int):
|
|
233
|
+
return None
|
|
234
|
+
return self.d.to_py(x.objs[0])
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ProdD(Desc):
|
|
238
|
+
def __init__(self, a, b):
|
|
239
|
+
self.a = a
|
|
240
|
+
self.b = b
|
|
241
|
+
|
|
242
|
+
def to_lean(self, v):
|
|
243
|
+
if not isinstance(v, (tuple, list)) or len(v) != 2:
|
|
244
|
+
_fail("expected a pair", v)
|
|
245
|
+
try:
|
|
246
|
+
a = self.a.to_lean(v[0])
|
|
247
|
+
except _Fail as f:
|
|
248
|
+
raise _at(0, f) from None
|
|
249
|
+
try:
|
|
250
|
+
b = self.b.to_lean(v[1])
|
|
251
|
+
except _Fail as f:
|
|
252
|
+
raise _at(1, f) from None
|
|
253
|
+
return Ctor(0, [a, b])
|
|
254
|
+
|
|
255
|
+
def to_py(self, x):
|
|
256
|
+
return (self.a.to_py(x.objs[0]), self.b.to_py(x.objs[1]))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class NamedD(Desc):
|
|
260
|
+
"""A generated type, looked up by name in the generated module (types may be recursive)."""
|
|
261
|
+
|
|
262
|
+
def __init__(self, g, name):
|
|
263
|
+
self.g = g
|
|
264
|
+
self.name = name
|
|
265
|
+
self._info = None
|
|
266
|
+
|
|
267
|
+
def info(self):
|
|
268
|
+
if self._info is None:
|
|
269
|
+
first, *rest = self.name.split(".")
|
|
270
|
+
t = self.g[first]
|
|
271
|
+
for r in rest:
|
|
272
|
+
t = getattr(t, r)
|
|
273
|
+
meta = getattr(t, "__metadata__", ())
|
|
274
|
+
infos = [m for m in meta if isinstance(m, SubtypeInfo)]
|
|
275
|
+
self._info = infos[0] if infos else t.__lean__
|
|
276
|
+
return self._info
|
|
277
|
+
|
|
278
|
+
def to_lean(self, v):
|
|
279
|
+
return self.info().to_lean(v)
|
|
280
|
+
|
|
281
|
+
def to_py(self, x):
|
|
282
|
+
return self.info().to_py(x)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
NAT = _Nat()
|
|
286
|
+
INT = _Int()
|
|
287
|
+
BOOL = _Bool()
|
|
288
|
+
CHAR = _Char()
|
|
289
|
+
STRING = _String()
|
|
290
|
+
FLOAT = _Float()
|
|
291
|
+
FLOAT32 = _Float32()
|
|
292
|
+
UNIT = _Unit()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def to_lean(desc, v, name):
|
|
296
|
+
try:
|
|
297
|
+
return desc.to_lean(v)
|
|
298
|
+
except _Fail as f:
|
|
299
|
+
f.loc = (name, *f.loc)
|
|
300
|
+
raise _validation_error("lean-python", f) from None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def to_py(desc, x):
|
|
304
|
+
return desc.to_py(x)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def io_result(r, desc, error_to_string):
|
|
308
|
+
if r.tag == 0:
|
|
309
|
+
return desc.to_py(r.objs[0])
|
|
310
|
+
raise LeanIOError(error_to_string(r.objs[0]).decode("utf-8"))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# Structures and constructors
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Obj:
|
|
318
|
+
__slots__ = ("i",)
|
|
319
|
+
|
|
320
|
+
def __init__(self, i):
|
|
321
|
+
self.i = i
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class Scalar:
|
|
325
|
+
__slots__ = ("key",)
|
|
326
|
+
|
|
327
|
+
def __init__(self, key):
|
|
328
|
+
self.key = key
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
ERASED = None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class FieldInfo:
|
|
335
|
+
__slots__ = ("desc", "loc", "name")
|
|
336
|
+
|
|
337
|
+
def __init__(self, name, loc, desc):
|
|
338
|
+
self.name = name
|
|
339
|
+
self.loc = loc
|
|
340
|
+
self.desc = desc
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class Invariant:
|
|
344
|
+
__slots__ = ("args", "field", "fn", "prop")
|
|
345
|
+
|
|
346
|
+
def __init__(self, field, prop, fn, args):
|
|
347
|
+
self.field = field
|
|
348
|
+
self.prop = prop
|
|
349
|
+
self.fn = fn
|
|
350
|
+
self.args = args
|
|
351
|
+
|
|
352
|
+
def holds(self, values):
|
|
353
|
+
return bool(self.fn(*[values[i] for i in self.args]))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class CtorInfo:
|
|
357
|
+
def __init__(self, name, cidx, size, unit, fields, invariants, positional):
|
|
358
|
+
self.name = name
|
|
359
|
+
self.cidx = cidx
|
|
360
|
+
self.size = size
|
|
361
|
+
self.unit = unit
|
|
362
|
+
self.fields = fields
|
|
363
|
+
self.invariants = invariants
|
|
364
|
+
self.positional = positional
|
|
365
|
+
|
|
366
|
+
def lean_values(self, v):
|
|
367
|
+
values = []
|
|
368
|
+
for f in self.fields:
|
|
369
|
+
try:
|
|
370
|
+
values.append(f.desc.to_lean(getattr(v, f.name)))
|
|
371
|
+
except _Fail as e:
|
|
372
|
+
raise _at(f.name, e) from None
|
|
373
|
+
for inv in self.invariants:
|
|
374
|
+
if not inv.holds(values):
|
|
375
|
+
_fail(f"{inv.field}: {inv.prop} does not hold", v)
|
|
376
|
+
return values
|
|
377
|
+
|
|
378
|
+
def build(self, values):
|
|
379
|
+
if self.unit:
|
|
380
|
+
return self.cidx
|
|
381
|
+
objs = [0] * self.size
|
|
382
|
+
o = Ctor(self.cidx, objs)
|
|
383
|
+
for f, x in zip(self.fields, values, strict=True):
|
|
384
|
+
loc = f.loc
|
|
385
|
+
if loc.__class__ is Obj:
|
|
386
|
+
objs[loc.i] = x
|
|
387
|
+
elif loc.__class__ is Scalar:
|
|
388
|
+
o.sc[loc.key] = x
|
|
389
|
+
return o
|
|
390
|
+
|
|
391
|
+
def read(self, cls, x):
|
|
392
|
+
kw = {}
|
|
393
|
+
for f in self.fields:
|
|
394
|
+
loc = f.loc
|
|
395
|
+
if loc.__class__ is Obj:
|
|
396
|
+
kw[f.name] = f.desc.to_py(x.objs[loc.i])
|
|
397
|
+
elif loc.__class__ is Scalar:
|
|
398
|
+
kw[f.name] = f.desc.to_py(x.sc[loc.key])
|
|
399
|
+
return cls.model_construct(**kw)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class StructInfo:
|
|
403
|
+
def __init__(self, cls, ctor, trivial=False):
|
|
404
|
+
self.cls = cls
|
|
405
|
+
self.ctor = ctor
|
|
406
|
+
self.trivial = trivial
|
|
407
|
+
cls.__lean_ctor__ = ctor
|
|
408
|
+
|
|
409
|
+
def to_lean(self, v):
|
|
410
|
+
if not isinstance(v, self.cls):
|
|
411
|
+
try:
|
|
412
|
+
v = self.cls.model_validate(v)
|
|
413
|
+
except ValidationError as e:
|
|
414
|
+
_fail(str(e), v)
|
|
415
|
+
values = self.ctor.lean_values(v)
|
|
416
|
+
if self.trivial:
|
|
417
|
+
return values[0]
|
|
418
|
+
return self.ctor.build(values)
|
|
419
|
+
|
|
420
|
+
def to_py(self, x):
|
|
421
|
+
if self.trivial:
|
|
422
|
+
f = self.ctor.fields[0]
|
|
423
|
+
return self.cls.model_construct(**{f.name: f.desc.to_py(x)})
|
|
424
|
+
return self.ctor.read(self.cls, x)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class SubtypeInfo:
|
|
428
|
+
def __init__(self, desc, invariant):
|
|
429
|
+
self.desc = desc
|
|
430
|
+
self.invariant = invariant
|
|
431
|
+
|
|
432
|
+
def to_lean(self, v):
|
|
433
|
+
x = self.desc.to_lean(v)
|
|
434
|
+
if not self.invariant.holds([x]):
|
|
435
|
+
_fail(f"{self.invariant.prop} does not hold", v)
|
|
436
|
+
return x
|
|
437
|
+
|
|
438
|
+
def to_py(self, x):
|
|
439
|
+
return self.desc.to_py(x)
|
|
440
|
+
|
|
441
|
+
def __call__(self, v):
|
|
442
|
+
try:
|
|
443
|
+
self.to_lean(v)
|
|
444
|
+
except _Fail as f:
|
|
445
|
+
raise ValueError(f.msg) from None
|
|
446
|
+
return v
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def adopt(cls, ctor, trivial=False):
|
|
450
|
+
"""Attaches the layout of the Lean structure made from the user's pydantic model ``cls``."""
|
|
451
|
+
cls.__lean__ = StructInfo(cls, ctor, trivial)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def subtype(pytype, desc, invariant, annotations=()):
|
|
455
|
+
info = SubtypeInfo(desc, invariant)
|
|
456
|
+
return Annotated[pytype, *annotations, AfterValidator(info), info]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def is_none(v):
|
|
460
|
+
return v is None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class LeanModel(BaseModel):
|
|
464
|
+
"""A Lean structure or constructor: frozen, strict, no extra fields."""
|
|
465
|
+
|
|
466
|
+
model_config = ConfigDict(
|
|
467
|
+
frozen=True,
|
|
468
|
+
strict=True,
|
|
469
|
+
extra="forbid",
|
|
470
|
+
validate_by_name=True,
|
|
471
|
+
validate_by_alias=True,
|
|
472
|
+
serialize_by_alias=True,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
@pydantic.model_validator(mode="after")
|
|
476
|
+
def _lean_invariants(self):
|
|
477
|
+
ctor = type(self).__dict__.get("__lean_ctor__")
|
|
478
|
+
if ctor is not None and ctor.invariants:
|
|
479
|
+
try:
|
|
480
|
+
ctor.lean_values(self)
|
|
481
|
+
except _Fail as f:
|
|
482
|
+
raise ValueError(f.msg) from None
|
|
483
|
+
return self
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class LeanEnum(enum.StrEnum):
|
|
487
|
+
"""A Lean inductive type whose constructors take no arguments."""
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class EnumInfo:
|
|
491
|
+
def __init__(self, cls, plain=False):
|
|
492
|
+
self.cls = cls
|
|
493
|
+
self.plain = plain
|
|
494
|
+
self.members = list(cls)
|
|
495
|
+
self.index = {m: i for i, m in enumerate(self.members)}
|
|
496
|
+
|
|
497
|
+
def to_lean(self, v):
|
|
498
|
+
if not isinstance(v, self.cls):
|
|
499
|
+
try:
|
|
500
|
+
v = self.cls(v)
|
|
501
|
+
except ValueError:
|
|
502
|
+
_fail(f"expected a {self.cls.__name__}", v)
|
|
503
|
+
return self.index[v]
|
|
504
|
+
|
|
505
|
+
def to_py(self, x):
|
|
506
|
+
m = self.members[x]
|
|
507
|
+
return m.value if self.plain else m
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def adopt_enum(cls):
|
|
511
|
+
"""Attaches the Lean inductive type made from the user's enum ``cls`` (a constructor per member)."""
|
|
512
|
+
cls.__lean__ = EnumInfo(cls)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class ModelUnion:
|
|
516
|
+
"""A union of the user's models (``Cat | Dog``): a Lean inductive type with a constructor per model."""
|
|
517
|
+
|
|
518
|
+
def __init__(self, alternatives):
|
|
519
|
+
self.alternatives = alternatives
|
|
520
|
+
self.__lean__ = self
|
|
521
|
+
|
|
522
|
+
def to_lean(self, v):
|
|
523
|
+
for cidx, d in self.alternatives:
|
|
524
|
+
if isinstance(v, d.info().cls):
|
|
525
|
+
return Ctor(cidx, [d.to_lean(v)])
|
|
526
|
+
_fail("expected one of " + ", ".join(d.info().cls.__name__ for _, d in self.alternatives), v)
|
|
527
|
+
|
|
528
|
+
def to_py(self, x):
|
|
529
|
+
for cidx, d in self.alternatives:
|
|
530
|
+
if cidx == x.tag:
|
|
531
|
+
return d.to_py(x.objs[0])
|
|
532
|
+
raise AssertionError(x.tag)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ---------------------------------------------------------------------------
|
|
536
|
+
# Inductive types: a model per constructor, JSON as Lean's `deriving ToJson, FromJson`
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class LeanInductive(BaseModel):
|
|
540
|
+
"""Base class of the constructor models of a Lean inductive type.
|
|
541
|
+
|
|
542
|
+
As a pydantic type it accepts an instance of a constructor model or Lean's JSON encoding:
|
|
543
|
+
``"ctor"`` for a constructor without arguments, ``{"ctor": {...}}`` for named arguments,
|
|
544
|
+
``{"ctor": value}`` or ``{"ctor": [v1, v2]}`` for unnamed ones.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
model_config = ConfigDict(frozen=True, strict=True, extra="forbid")
|
|
548
|
+
|
|
549
|
+
__lean_ctors__: ClassVar[dict] = {}
|
|
550
|
+
|
|
551
|
+
@classmethod
|
|
552
|
+
def _lean_payload(cls, sub, payload):
|
|
553
|
+
info = sub.__lean_ctor__
|
|
554
|
+
names = [f.name for f in info.fields]
|
|
555
|
+
if info.positional and names:
|
|
556
|
+
if len(names) == 1:
|
|
557
|
+
return {names[0]: payload}
|
|
558
|
+
if not isinstance(payload, (list, tuple)) or len(payload) != len(names):
|
|
559
|
+
raise PydanticCustomError("lean_tag", "expected {n} arguments", {"n": len(names)})
|
|
560
|
+
return dict(zip(names, payload, strict=True))
|
|
561
|
+
if not names and payload in (None, {}):
|
|
562
|
+
return {}
|
|
563
|
+
return payload
|
|
564
|
+
|
|
565
|
+
@classmethod
|
|
566
|
+
def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler):
|
|
567
|
+
if "__lean_ctors__" not in cls.__dict__:
|
|
568
|
+
return handler(source)
|
|
569
|
+
ctors = cls.__lean_ctors__
|
|
570
|
+
|
|
571
|
+
def validate(v, info):
|
|
572
|
+
if isinstance(v, cls):
|
|
573
|
+
return v
|
|
574
|
+
if isinstance(v, str):
|
|
575
|
+
tag, payload = v, None
|
|
576
|
+
elif isinstance(v, dict) and len(v) == 1:
|
|
577
|
+
((tag, payload),) = v.items()
|
|
578
|
+
else:
|
|
579
|
+
raise PydanticCustomError(
|
|
580
|
+
"lean_tag", "expected a constructor of {t}: a name or an object with one key", {"t": cls.__name__}
|
|
581
|
+
)
|
|
582
|
+
sub = ctors.get(tag)
|
|
583
|
+
if sub is None:
|
|
584
|
+
raise PydanticCustomError(
|
|
585
|
+
"lean_tag", "unknown constructor {tag} of {t}", {"tag": tag, "t": cls.__name__}
|
|
586
|
+
)
|
|
587
|
+
data = cls._lean_payload(sub, payload)
|
|
588
|
+
if info.mode == "json":
|
|
589
|
+
return sub.model_validate_json(json.dumps(data))
|
|
590
|
+
return sub.model_validate(data)
|
|
591
|
+
|
|
592
|
+
def serialize(v, info):
|
|
593
|
+
sub = type(v)
|
|
594
|
+
ci = sub.__lean_ctor__
|
|
595
|
+
if ci.unit or not ci.fields:
|
|
596
|
+
return ci.name
|
|
597
|
+
d = v.model_dump(mode="json" if info.mode == "json" else "python")
|
|
598
|
+
if ci.positional:
|
|
599
|
+
vals = [d[f.name] for f in ci.fields]
|
|
600
|
+
return {ci.name: vals[0] if len(vals) == 1 else vals}
|
|
601
|
+
return {ci.name: d}
|
|
602
|
+
|
|
603
|
+
def json_schema(_schema, handler):
|
|
604
|
+
return cls._lean_json_schema(handler)
|
|
605
|
+
|
|
606
|
+
return core_schema.with_info_plain_validator_function(
|
|
607
|
+
validate,
|
|
608
|
+
serialization=core_schema.plain_serializer_function_ser_schema(serialize, info_arg=True),
|
|
609
|
+
metadata={"pydantic_js_functions": [json_schema]},
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
@classmethod
|
|
613
|
+
def __get_pydantic_json_schema__(cls, schema, handler: GetJsonSchemaHandler):
|
|
614
|
+
if "__lean_ctors__" not in cls.__dict__:
|
|
615
|
+
return super().__get_pydantic_json_schema__(schema, handler)
|
|
616
|
+
return cls._lean_json_schema(handler)
|
|
617
|
+
|
|
618
|
+
@classmethod
|
|
619
|
+
def _lean_json_schema(cls, handler: GetJsonSchemaHandler):
|
|
620
|
+
variants = []
|
|
621
|
+
for tag, sub in cls.__lean_ctors__.items():
|
|
622
|
+
ci = sub.__lean_ctor__
|
|
623
|
+
if ci.unit or not ci.fields:
|
|
624
|
+
variants.append({"const": tag})
|
|
625
|
+
continue
|
|
626
|
+
payload = _inline_defs(sub.model_json_schema(mode=getattr(handler, "mode", "validation")))
|
|
627
|
+
if ci.positional:
|
|
628
|
+
props = payload.get("properties", {})
|
|
629
|
+
items = [props.get(f.name, {}) for f in ci.fields]
|
|
630
|
+
payload = (
|
|
631
|
+
items[0]
|
|
632
|
+
if len(items) == 1
|
|
633
|
+
else {"type": "array", "prefixItems": items, "minItems": len(items), "maxItems": len(items)}
|
|
634
|
+
)
|
|
635
|
+
variants.append(
|
|
636
|
+
{"type": "object", "properties": {tag: payload}, "required": [tag], "additionalProperties": False}
|
|
637
|
+
)
|
|
638
|
+
return {"oneOf": variants, "title": cls.__name__, "description": cls.__doc__}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _inline_defs(schema):
|
|
642
|
+
"""The JSON Schema with its ``$defs`` inlined (a recursive reference stays a ``$ref``)."""
|
|
643
|
+
defs = schema.pop("$defs", {})
|
|
644
|
+
|
|
645
|
+
def go(node, seen):
|
|
646
|
+
if isinstance(node, dict):
|
|
647
|
+
ref = node.get("$ref")
|
|
648
|
+
if isinstance(ref, str) and ref.startswith("#/$defs/"):
|
|
649
|
+
name = ref.removeprefix("#/$defs/")
|
|
650
|
+
if name in seen or name not in defs:
|
|
651
|
+
return node
|
|
652
|
+
return go(defs[name], seen | {name})
|
|
653
|
+
return {k: go(v, seen) for k, v in node.items()}
|
|
654
|
+
if isinstance(node, list):
|
|
655
|
+
return [go(v, seen) for v in node]
|
|
656
|
+
return node
|
|
657
|
+
|
|
658
|
+
return go(schema, frozenset())
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class InductiveInfo:
|
|
662
|
+
def __init__(self, base, pairs):
|
|
663
|
+
self.base = base
|
|
664
|
+
self.by_tag = {}
|
|
665
|
+
for sub, ci in pairs:
|
|
666
|
+
self.by_tag[ci.cidx] = (sub, ci)
|
|
667
|
+
|
|
668
|
+
def to_lean(self, v):
|
|
669
|
+
if not isinstance(v, self.base):
|
|
670
|
+
try:
|
|
671
|
+
v = pydantic.TypeAdapter(self.base).validate_python(v)
|
|
672
|
+
except ValidationError as e:
|
|
673
|
+
_fail(str(e), v)
|
|
674
|
+
ci = type(v).__lean_ctor__
|
|
675
|
+
return ci.build(ci.lean_values(v))
|
|
676
|
+
|
|
677
|
+
def to_py(self, x):
|
|
678
|
+
tag = x if isinstance(x, int) else x.tag
|
|
679
|
+
sub, ci = self.by_tag[tag]
|
|
680
|
+
if ci.unit:
|
|
681
|
+
return sub.model_construct()
|
|
682
|
+
return ci.read(sub, x)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def register_inductive(base, pairs):
|
|
686
|
+
ctors = {}
|
|
687
|
+
for sub, ci in pairs:
|
|
688
|
+
sub.__lean_ctor__ = ci
|
|
689
|
+
sub.__name__ = ci.name
|
|
690
|
+
setattr(base, ci.name, sub)
|
|
691
|
+
ctors[ci.name] = sub
|
|
692
|
+
base.__lean_ctors__ = ctors
|
|
693
|
+
base.__lean__ = InductiveInfo(base, pairs)
|
|
694
|
+
schema = base.__get_pydantic_core_schema__(base, None)
|
|
695
|
+
base.__pydantic_core_schema__ = schema
|
|
696
|
+
base.__pydantic_validator__ = SchemaValidator(schema)
|
|
697
|
+
base.__pydantic_serializer__ = SchemaSerializer(schema)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# ---------------------------------------------------------------------------
|
|
701
|
+
# Module assembly
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def attach(g, path, fn):
|
|
705
|
+
fn.__name__ = path[-1]
|
|
706
|
+
fn.__qualname__ = ".".join(path)
|
|
707
|
+
if len(path) == 1:
|
|
708
|
+
g[path[0]] = fn
|
|
709
|
+
return
|
|
710
|
+
holder = g.get(path[0])
|
|
711
|
+
if holder is None:
|
|
712
|
+
holder = type(path[0], (), {"__module__": g.get("__name__")})
|
|
713
|
+
g[path[0]] = holder
|
|
714
|
+
for p in path[1:-1]:
|
|
715
|
+
nxt = getattr(holder, p, None)
|
|
716
|
+
if nxt is None:
|
|
717
|
+
nxt = type(p, (), {"__module__": g.get("__name__")})
|
|
718
|
+
setattr(holder, p, nxt)
|
|
719
|
+
holder = nxt
|
|
720
|
+
setattr(holder, path[-1], staticmethod(fn))
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def finish(g):
|
|
724
|
+
for v in list(g.values()):
|
|
725
|
+
if isinstance(v, type) and issubclass(v, LeanModel) and v is not LeanModel:
|
|
726
|
+
v.model_rebuild(force=True, _types_namespace=g)
|
|
727
|
+
v.__match_args__ = tuple(v.model_fields)
|