detree-vm 0.1.0__py3-none-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.
Files changed (49) hide show
  1. detree/__init__.py +66 -0
  2. detree/__main__.py +14 -0
  3. detree/_libs/rustdetree.dll +0 -0
  4. detree/_version.py +24 -0
  5. detree/abi/__init__.py +13 -0
  6. detree/abi/capi.py +552 -0
  7. detree/abi/libs.py +178 -0
  8. detree/backends/__init__.py +24 -0
  9. detree/backends/base.py +109 -0
  10. detree/backends/cpp.py +49 -0
  11. detree/backends/policy.py +29 -0
  12. detree/backends/python.py +47 -0
  13. detree/backends/registry.py +189 -0
  14. detree/backends/rust.py +48 -0
  15. detree/cli.py +205 -0
  16. detree/include/detree.h +211 -0
  17. detree/ir/__init__.py +27 -0
  18. detree/ir/builder.py +321 -0
  19. detree/ir/dump.py +240 -0
  20. detree/ir/errors.py +18 -0
  21. detree/ir/nodes.py +939 -0
  22. detree/ir/verify.py +647 -0
  23. detree/lower/__init__.py +207 -0
  24. detree/lower/codegen.py +81 -0
  25. detree/lower/linearize.py +692 -0
  26. detree/lower/regalloc.py +40 -0
  27. detree/machine.py +195 -0
  28. detree/opt/__init__.py +13 -0
  29. detree/opt/common.py +421 -0
  30. detree/opt/const.py +201 -0
  31. detree/opt/dead.py +182 -0
  32. detree/opt/fuse.py +985 -0
  33. detree/opt/inline.py +186 -0
  34. detree/opt/loops.py +88 -0
  35. detree/opt/manager.py +148 -0
  36. detree/opt/peephole.py +104 -0
  37. detree/py.typed +4 -0
  38. detree/spec/__init__.py +15 -0
  39. detree/spec/errors.py +129 -0
  40. detree/spec/isa.py +430 -0
  41. detree/spec/module.py +550 -0
  42. detree/spec/opinfo.py +116 -0
  43. detree/spec/validate.py +359 -0
  44. detree/vm/__init__.py +13 -0
  45. detree/vm/python.py +1076 -0
  46. detree_vm-0.1.0.dist-info/METADATA +75 -0
  47. detree_vm-0.1.0.dist-info/RECORD +49 -0
  48. detree_vm-0.1.0.dist-info/WHEEL +5 -0
  49. detree_vm-0.1.0.dist-info/licenses/LICENSE +21 -0
detree/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ """
2
+ Detree: a domain-specific bytecode VM for TTree binary streams.
3
+
4
+ The package is layered so the machine can be reimplemented and measured:
5
+
6
+ - `detree.spec` — the module format, opcode tables, validator, errors;
7
+ - `detree.ir` — the typed tree IR a compiler builds;
8
+ - `detree.opt` — optimizer passes over the IR;
9
+ - `detree.lower` — IR to module bytes;
10
+ - `detree.vm` — the Python reference machine;
11
+ - `detree.abi` — the `ctypes` binding to the compiled machines;
12
+ - `detree.backends` — the backend registry and adapters.
13
+
14
+ The public surface is two calls: `compile` turns an IR program into module
15
+ bytes, and `Machine` builds one machine from those bytes. The instance belongs
16
+ to the caller, who decides how many to build and which thread runs each.
17
+ Importing the package is cheap: NumPy and the compiled libraries are imported
18
+ when a machine is built, not here.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import TYPE_CHECKING
24
+
25
+ if TYPE_CHECKING:
26
+ from pathlib import Path
27
+
28
+ try: # written by setuptools_scm at build time
29
+ from ._version import version as __version__
30
+ except ImportError: # a wheel or source checkout without the generated file
31
+ from importlib.metadata import PackageNotFoundError, version
32
+
33
+ try:
34
+ __version__ = version("detree-vm")
35
+ except PackageNotFoundError:
36
+ __version__ = "0.1.0.dev0"
37
+
38
+ from .machine import Machine, compile
39
+
40
+ # The public API, in documentation order.
41
+ PUBLIC_API = (
42
+ "Machine",
43
+ "compile",
44
+ "include_dir",
45
+ )
46
+
47
+
48
+ def include_dir() -> Path:
49
+ """
50
+ Return the directory that contains `detree.h`.
51
+
52
+ The release wheel ships the header, so a C or Rust embedder can build
53
+ against the library it installed without a source checkout.
54
+ """
55
+ from .abi.libs import include_dir as _include_dir
56
+
57
+ return _include_dir()
58
+
59
+
60
+ __all__ = [
61
+ "PUBLIC_API",
62
+ "Machine",
63
+ "__version__",
64
+ "compile",
65
+ "include_dir",
66
+ ]
detree/__main__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ `python -m detree`.
3
+
4
+ Delegates to `detree.cli.main` and returns its exit status.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ from .cli import main
12
+
13
+ if __name__ == "__main__":
14
+ sys.exit(main())
Binary file
detree/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = 'v0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
detree/abi/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ The Python side of the C ABI.
3
+
4
+ One `ctypes` binding serves both compiled backends: it is parameterized by the
5
+ loaded library, not by the implementation. The C contract is defined in
6
+ `docs/spec/c-api.md` and in `kernel/include/detree.h`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .libs import include_dir, library_path
12
+
13
+ __all__ = ["include_dir", "library_path"]
detree/abi/capi.py ADDED
@@ -0,0 +1,552 @@
1
+ """
2
+ `ctypes` binding to the C ABI.
3
+
4
+ This module declares the structures and the `detree_*` entry points of
5
+ `kernel/include/detree.h`. The declarations are pinned by a layout test:
6
+ a C probe prints `sizeof`/`offsetof` and the test compares it with these
7
+ definitions.
8
+
9
+ The machine below is a thin adapter, not an implementation: it converts Python
10
+ objects to C arguments, calls one entry point per basket, and wraps the
11
+ returned bytes in NumPy arrays that keep the C owner alive. All semantics live
12
+ on the other side of the boundary.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import ctypes
18
+ import functools
19
+ import os
20
+ from collections.abc import Mapping, Sequence
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+
25
+ from ..spec.errors import (
26
+ AssertionFailed,
27
+ BackendUnavailable,
28
+ DetreeError,
29
+ InvalidByteCount,
30
+ ModuleError,
31
+ ReadLengthMismatch,
32
+ StackOverflow,
33
+ StreamUnderflow,
34
+ UnsupportedFeature,
35
+ )
36
+ from .libs import library_path
37
+
38
+ # `DETREE_C_API_VERSION` in the header.
39
+ C_API_VERSION = 1
40
+
41
+ # `detree_status` value -> the Python class of the same meaning. A status of
42
+ # 1 (module error) is special-cased at run time, where it means "the module is
43
+ # malformed", not "the module failed validation".
44
+ STATUS_TO_EXCEPTION = {
45
+ 0: None, # DETREE_OK
46
+ 1: ModuleError, # DETREE_MODULE_ERROR (decode or validation)
47
+ 2: StreamUnderflow,
48
+ 3: InvalidByteCount,
49
+ 4: ReadLengthMismatch,
50
+ 5: UnsupportedFeature,
51
+ 6: AssertionFailed,
52
+ 7: StackOverflow,
53
+ 8: DetreeError,
54
+ 9: DetreeError, # DETREE_BAD_ARGUMENT
55
+ }
56
+
57
+
58
+ class _DetreeError(ctypes.Structure):
59
+ """
60
+ Mirrors `detree_error` in `detree.h`.
61
+
62
+ Pointers are borrowed, so they are kept as addresses.
63
+ """
64
+
65
+ _fields_ = [
66
+ ("status", ctypes.c_int32),
67
+ ("reserved", ctypes.c_int32),
68
+ ("ip", ctypes.c_int64),
69
+ ("cursor", ctypes.c_int64),
70
+ ("message", ctypes.c_void_p),
71
+ ("message_length", ctypes.c_size_t),
72
+ ("function", ctypes.c_void_p),
73
+ ("function_length", ctypes.c_size_t),
74
+ ]
75
+
76
+
77
+ class _DetreeOutput(ctypes.Structure):
78
+ """
79
+ Mirrors `detree_output` in `detree.h`.
80
+
81
+ `owner` is retained until the array built from these bytes is dropped.
82
+ """
83
+
84
+ _fields_ = [
85
+ ("data", ctypes.c_void_p),
86
+ ("length", ctypes.c_size_t),
87
+ ("itemsize", ctypes.c_size_t),
88
+ ("owner", ctypes.c_void_p),
89
+ ]
90
+
91
+
92
+ class _DetreeState(ctypes.Structure):
93
+ """
94
+ Mirrors `detree_state` in `detree.h`.
95
+
96
+ `name` and `text` are borrowed from the machine.
97
+ """
98
+
99
+ _fields_ = [
100
+ ("name", ctypes.c_void_p),
101
+ ("name_length", ctypes.c_size_t),
102
+ ("is_name", ctypes.c_int32),
103
+ ("reserved", ctypes.c_uint32),
104
+ ("value", ctypes.c_uint64),
105
+ ("text", ctypes.c_void_p),
106
+ ("text_length", ctypes.c_size_t),
107
+ ]
108
+
109
+
110
+ _U8P = ctypes.POINTER(ctypes.c_uint8)
111
+ _I64P = ctypes.POINTER(ctypes.c_int64)
112
+
113
+
114
+ class _Owner:
115
+ """
116
+ Keeps one retained buffer storage alive, and releases it on collection.
117
+
118
+ The ABI hands out borrowed bytes; keeping the owner is what lets an array
119
+ outlive the machine's next run.
120
+ """
121
+
122
+ __slots__ = ("_library", "_ptr")
123
+
124
+ def __init__(self, library: _Library, ptr: int) -> None:
125
+ self._library = library
126
+ self._ptr = int(ptr)
127
+
128
+ def __del__(self) -> None:
129
+ ptr = getattr(self, "_ptr", None)
130
+ if not ptr:
131
+ return
132
+ self._ptr = None
133
+ try:
134
+ self._library.lib.detree_owner_release(ptr)
135
+ except Exception: # pragma: no cover - interpreter shutdown
136
+ pass
137
+
138
+
139
+ class _OwnedArray(np.ndarray):
140
+ """
141
+ A NumPy view whose `_owner` keeps the C storage alive.
142
+
143
+ The attribute is set after the view is built, which a NumPy subclass
144
+ allows because the class declares no `__slots__`.
145
+ """
146
+
147
+
148
+ def _text(ptr: int | None, length: int) -> str:
149
+ if not ptr:
150
+ return ""
151
+ return ctypes.string_at(ptr, length).decode("utf-8", "replace")
152
+
153
+
154
+ def _u8_view(data: bytes | bytearray | memoryview | np.ndarray) -> np.ndarray:
155
+ """
156
+ *data* as a contiguous `uint8` array, without copying what already is one.
157
+
158
+ A basket is tens of kilobytes and the machine only reads it, so copying it
159
+ per call is pure overhead: the returned array owns or borrows the caller's
160
+ buffer, and the caller keeps it alive for the duration of the call.
161
+ """
162
+ if isinstance(data, np.ndarray):
163
+ if data.dtype == np.uint8 and data.flags["C_CONTIGUOUS"]:
164
+ return data
165
+ return np.ascontiguousarray(data, dtype=np.uint8)
166
+ return np.frombuffer(memoryview(data).cast("B"), dtype=np.uint8)
167
+
168
+
169
+ def _i64_view(offsets: Sequence[int] | np.ndarray) -> np.ndarray:
170
+ """
171
+ *offsets* as a contiguous `int64` array, copying only when it is not one.
172
+
173
+ Entry offsets are the one argument with one element per event, so building
174
+ them element by element in Python costs more than decoding the basket.
175
+ """
176
+ if isinstance(offsets, np.ndarray):
177
+ if offsets.dtype == np.int64 and offsets.flags["C_CONTIGUOUS"]:
178
+ return offsets
179
+ return np.ascontiguousarray(offsets, dtype=np.int64)
180
+ return np.ascontiguousarray(offsets, dtype=np.int64)
181
+
182
+
183
+ class _Library:
184
+ """
185
+ One loaded shared library, with the prototypes it is expected to export.
186
+
187
+ Construction checks the C ABI version and the implementation name, so a
188
+ library with the wrong identity fails here instead of inside a basket.
189
+ """
190
+
191
+ def __init__(self, path: Path, backend: str) -> None:
192
+ self.path = path
193
+ self.backend = backend
194
+ try:
195
+ self.lib = ctypes.CDLL(str(path))
196
+ except OSError as error: # pragma: no cover - depends on the platform
197
+ raise BackendUnavailable(f"cannot load {path}: {error}") from error
198
+
199
+ self._declare_prototypes()
200
+
201
+ version = self.lib.detree_c_api_version()
202
+ if version != C_API_VERSION:
203
+ raise BackendUnavailable(
204
+ f"{path} implements C ABI {version}, expected {C_API_VERSION}"
205
+ )
206
+ name = self.lib.detree_backend_name()
207
+ if name is None or name.decode("ascii") != backend:
208
+ found = "<none>" if name is None else name.decode("ascii", "replace")
209
+ raise BackendUnavailable(f"{path} is the {found!r} machine, not {backend!r}")
210
+
211
+ # -- declarations -----------------------------------------------------
212
+ def _declare_prototypes(self) -> None:
213
+ lib = self.lib
214
+
215
+ def declare(name: str, restype: object, argtypes: list[object]) -> None:
216
+ function = getattr(lib, name)
217
+ function.restype = restype
218
+ function.argtypes = argtypes
219
+
220
+ declare("detree_c_api_version", ctypes.c_uint32, [])
221
+ declare("detree_backend_name", ctypes.c_char_p, [])
222
+ declare("detree_backend_version", ctypes.c_char_p, [])
223
+ declare("detree_isa_major", ctypes.c_uint16, [])
224
+ declare("detree_isa_minor", ctypes.c_uint16, [])
225
+ declare("detree_capabilities", ctypes.c_uint64, [])
226
+
227
+ declare("detree_machine_new", ctypes.c_void_p, [_U8P, ctypes.c_size_t])
228
+ declare("detree_machine_free", None, [ctypes.c_void_p])
229
+ declare("detree_machine_reset", None, [ctypes.c_void_p])
230
+ declare("detree_machine_isa_major", ctypes.c_uint16, [ctypes.c_void_p])
231
+ declare("detree_machine_isa_minor", ctypes.c_uint16, [ctypes.c_void_p])
232
+
233
+ declare("detree_buffer_count", ctypes.c_size_t, [ctypes.c_void_p])
234
+ declare(
235
+ "detree_buffer_name",
236
+ ctypes.c_void_p,
237
+ [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)],
238
+ )
239
+ declare(
240
+ "detree_buffer_dtype",
241
+ ctypes.c_void_p,
242
+ [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)],
243
+ )
244
+
245
+ declare(
246
+ "detree_run",
247
+ ctypes.c_int32,
248
+ [
249
+ ctypes.c_void_p,
250
+ _U8P,
251
+ ctypes.c_size_t,
252
+ _I64P,
253
+ ctypes.c_size_t,
254
+ ctypes.c_size_t,
255
+ ],
256
+ )
257
+ declare(
258
+ "detree_read",
259
+ ctypes.c_int32,
260
+ [
261
+ ctypes.c_void_p,
262
+ _U8P,
263
+ ctypes.c_size_t,
264
+ _I64P,
265
+ ctypes.c_size_t,
266
+ ctypes.c_size_t,
267
+ ctypes.POINTER(_DetreeOutput),
268
+ ctypes.c_size_t,
269
+ ctypes.POINTER(ctypes.c_size_t),
270
+ ],
271
+ )
272
+ declare(
273
+ "detree_acquire_outputs",
274
+ ctypes.c_size_t,
275
+ [ctypes.c_void_p, ctypes.POINTER(_DetreeOutput), ctypes.c_size_t],
276
+ )
277
+ declare("detree_owner_release", None, [ctypes.c_void_p])
278
+
279
+ declare("detree_state_count", ctypes.c_size_t, [ctypes.c_void_p])
280
+ declare(
281
+ "detree_state_name",
282
+ ctypes.c_void_p,
283
+ [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)],
284
+ )
285
+ declare(
286
+ "detree_get_states",
287
+ ctypes.c_size_t,
288
+ [ctypes.c_void_p, ctypes.POINTER(_DetreeState), ctypes.c_size_t],
289
+ )
290
+ declare("detree_last_error", _DetreeError, [])
291
+
292
+ # -- helpers ----------------------------------------------------------
293
+ def error(self, module_error: bool = False) -> DetreeError:
294
+ """
295
+ The last failure as a Python exception, with the machine's context.
296
+
297
+ Args:
298
+ module_error: report a decode or validation failure as
299
+ `ModuleError` regardless of the status code.
300
+
301
+ Returns:
302
+ The exception to raise.
303
+ """
304
+ error = self.lib.detree_last_error()
305
+ message = _text(error.message, error.message_length)
306
+ function = _text(error.function, error.function_length)
307
+ cls = STATUS_TO_EXCEPTION.get(error.status) or DetreeError
308
+ if module_error:
309
+ cls = ModuleError
310
+ elif error.status == 1:
311
+ # A module error raised during a run means execution left the
312
+ # function, which the reference reports as the base class.
313
+ cls = DetreeError
314
+ return cls(message, function=function, ip=error.ip, cursor=error.cursor)
315
+
316
+ def machine(self, module: bytes) -> CapiMachine:
317
+ """
318
+ Build a machine from module bytes.
319
+
320
+ Args:
321
+ module: the module byte format of `docs/spec/module.md`.
322
+
323
+ Returns:
324
+ A machine bound to this library.
325
+
326
+ Raises:
327
+ ModuleError: the module does not decode or does not validate.
328
+ """
329
+ raw = _u8_view(module)
330
+ ptr = self.lib.detree_machine_new(ctypes.cast(raw.ctypes.data, _U8P), raw.size)
331
+ if not ptr:
332
+ raise self.error(module_error=True)
333
+ return CapiMachine(self, ptr)
334
+
335
+
336
+ class CapiMachine:
337
+ """
338
+ The backend-neutral machine protocol over one compiled library.
339
+
340
+ One instance wraps one `detree_machine`; it is not thread-safe, because
341
+ the machine's registers and buffers are plain C state behind the pointer.
342
+ """
343
+
344
+ def __init__(self, library: _Library, ptr: int) -> None:
345
+ self._library = library
346
+ self._ptr = int(ptr)
347
+ self.backend = library.backend
348
+ self.isa = (
349
+ int(library.lib.detree_machine_isa_major(self._ptr)),
350
+ int(library.lib.detree_machine_isa_minor(self._ptr)),
351
+ )
352
+
353
+ lib = library.lib
354
+ self._buffer_names = []
355
+ self._buffer_dtypes = []
356
+ for index in range(lib.detree_buffer_count(self._ptr)):
357
+ length = ctypes.c_size_t()
358
+ name = lib.detree_buffer_name(self._ptr, index, ctypes.byref(length))
359
+ self._buffer_names.append(_text(name, length.value))
360
+ dtype = lib.detree_buffer_dtype(self._ptr, index, ctypes.byref(length))
361
+ self._buffer_dtypes.append(_text(dtype, length.value))
362
+
363
+ self._state_names = []
364
+ self._state_is_name = []
365
+ for index in range(lib.detree_state_count(self._ptr)):
366
+ length = ctypes.c_size_t()
367
+ name = lib.detree_state_name(self._ptr, index, ctypes.byref(length))
368
+ self._state_names.append(_text(name, length.value))
369
+ self._state_is_name.append(False)
370
+ self._read_state_kinds()
371
+
372
+ def _read_state_kinds(self) -> None:
373
+ """
374
+ Read the name/integer kind once, so `state_values` stays simple.
375
+
376
+ One `detree_get_states` call at build time costs less than one per
377
+ basket.
378
+ """
379
+ count = len(self._state_names)
380
+ if not count:
381
+ return
382
+ states = (_DetreeState * count)()
383
+ written = self._library.lib.detree_get_states(self._ptr, states, count)
384
+ for index in range(written):
385
+ self._state_is_name[index] = bool(states[index].is_name)
386
+
387
+ # -- protocol ---------------------------------------------------------
388
+ def run(
389
+ self,
390
+ data: bytes | bytearray | memoryview | np.ndarray,
391
+ offsets: Sequence[int] | np.ndarray,
392
+ cursor_offset: int = 0,
393
+ ) -> Mapping[str, np.ndarray]:
394
+ """
395
+ Run one basket and take its outputs.
396
+
397
+ Args:
398
+ data: the decompressed basket body.
399
+ offsets: cumulative entry offsets, at least two.
400
+ cursor_offset: the key length used for object references.
401
+
402
+ Returns:
403
+ One read-only array per buffer, keyed by buffer name.
404
+ """
405
+ self._require_open()
406
+ raw = _u8_view(data)
407
+ entries = _i64_view(offsets)
408
+
409
+ count = len(self._buffer_names)
410
+ outputs = (_DetreeOutput * count)()
411
+ written = ctypes.c_size_t(0)
412
+ status = self._library.lib.detree_read(
413
+ self._ptr,
414
+ ctypes.cast(raw.ctypes.data, _U8P),
415
+ raw.size,
416
+ entries.ctypes.data_as(_I64P),
417
+ entries.size,
418
+ int(cursor_offset),
419
+ outputs,
420
+ count,
421
+ ctypes.byref(written),
422
+ )
423
+ if status != 0:
424
+ raise self._library.error()
425
+
426
+ result = {}
427
+ for index in range(written.value):
428
+ output = outputs[index]
429
+ dtype = np.dtype(self._buffer_dtypes[index])
430
+ if output.length == 0:
431
+ if output.owner:
432
+ self._library.lib.detree_owner_release(output.owner)
433
+ result[self._buffer_names[index]] = np.empty(0, dtype=dtype)
434
+ continue
435
+
436
+ nbytes = output.length * output.itemsize
437
+ owner = _Owner(self._library, output.owner)
438
+ source = (ctypes.c_char * nbytes).from_address(output.data)
439
+ array = np.frombuffer(source, dtype=dtype).view(_OwnedArray)
440
+ array._owner = owner
441
+ result[self._buffer_names[index]] = array
442
+ return result
443
+
444
+ def state_values(self) -> Mapping[str, int | str | None]:
445
+ """
446
+ The state slots after the last run, with name slots resolved.
447
+
448
+ Returns:
449
+ One entry per declared state.
450
+ """
451
+ self._require_open()
452
+ count = len(self._state_names)
453
+ if not count:
454
+ return {}
455
+ states = (_DetreeState * count)()
456
+ written = self._library.lib.detree_get_states(self._ptr, states, count)
457
+ values = {}
458
+ for index in range(written):
459
+ state = states[index]
460
+ if state.is_name:
461
+ values[self._state_names[index]] = (
462
+ _text(state.text, state.text_length) if state.text else None
463
+ )
464
+ else:
465
+ values[self._state_names[index]] = int(state.value)
466
+ return values
467
+
468
+ def reset(self) -> None:
469
+ """
470
+ Reset the machine; buffer storage is reallocated, not cleared.
471
+
472
+ Arrays handed out by an earlier run stay readable after this call.
473
+ """
474
+ self._require_open()
475
+ self._library.lib.detree_machine_reset(self._ptr)
476
+
477
+ def close(self) -> None:
478
+ """
479
+ Free the machine; calling it twice is a no-op.
480
+
481
+ After the call every method raises `DetreeError`.
482
+ """
483
+ ptr = self._ptr
484
+ self._ptr = 0
485
+ if ptr:
486
+ self._library.lib.detree_machine_free(ptr)
487
+
488
+ def __del__(self) -> None:
489
+ try:
490
+ self.close()
491
+ except Exception: # pragma: no cover - interpreter shutdown
492
+ pass
493
+
494
+ def _require_open(self) -> None:
495
+ if not self._ptr:
496
+ raise DetreeError("machine is closed")
497
+
498
+
499
+ @functools.cache
500
+ def _load(path: str, backend: str) -> _Library:
501
+ return _Library(Path(path), backend)
502
+
503
+
504
+ def load(
505
+ backend: str = "cpp",
506
+ explicit: str | os.PathLike | None = None,
507
+ ) -> _Library:
508
+ """
509
+ Load the compiled library for *backend*.
510
+
511
+ Args:
512
+ backend: the implementation name to load.
513
+ explicit: an explicit library path, tried before the search order.
514
+
515
+ Returns:
516
+ The loaded library, cached per process.
517
+
518
+ Raises:
519
+ BackendUnavailable: the library is missing, or has the wrong identity.
520
+ """
521
+ path = library_path(backend, explicit)
522
+ if path is None:
523
+ raise BackendUnavailable(f"{backend} library was not found")
524
+ return _load(str(path), backend)
525
+
526
+
527
+ def build(
528
+ module: bytes,
529
+ backend: str = "cpp",
530
+ explicit: str | os.PathLike | None = None,
531
+ ) -> CapiMachine:
532
+ """
533
+ Decode *module* with the compiled machine named by *backend*.
534
+
535
+ Args:
536
+ module: the module byte format of `docs/spec/module.md`.
537
+ backend: the implementation name to call.
538
+ explicit: an explicit library path, tried before the search order.
539
+
540
+ Returns:
541
+ A machine bound to that library.
542
+ """
543
+ return load(backend, explicit).machine(module)
544
+
545
+
546
+ __all__ = [
547
+ "C_API_VERSION",
548
+ "CapiMachine",
549
+ "STATUS_TO_EXCEPTION",
550
+ "build",
551
+ "load",
552
+ ]