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.
- detree/__init__.py +66 -0
- detree/__main__.py +14 -0
- detree/_libs/rustdetree.dll +0 -0
- detree/_version.py +24 -0
- detree/abi/__init__.py +13 -0
- detree/abi/capi.py +552 -0
- detree/abi/libs.py +178 -0
- detree/backends/__init__.py +24 -0
- detree/backends/base.py +109 -0
- detree/backends/cpp.py +49 -0
- detree/backends/policy.py +29 -0
- detree/backends/python.py +47 -0
- detree/backends/registry.py +189 -0
- detree/backends/rust.py +48 -0
- detree/cli.py +205 -0
- detree/include/detree.h +211 -0
- detree/ir/__init__.py +27 -0
- detree/ir/builder.py +321 -0
- detree/ir/dump.py +240 -0
- detree/ir/errors.py +18 -0
- detree/ir/nodes.py +939 -0
- detree/ir/verify.py +647 -0
- detree/lower/__init__.py +207 -0
- detree/lower/codegen.py +81 -0
- detree/lower/linearize.py +692 -0
- detree/lower/regalloc.py +40 -0
- detree/machine.py +195 -0
- detree/opt/__init__.py +13 -0
- detree/opt/common.py +421 -0
- detree/opt/const.py +201 -0
- detree/opt/dead.py +182 -0
- detree/opt/fuse.py +985 -0
- detree/opt/inline.py +186 -0
- detree/opt/loops.py +88 -0
- detree/opt/manager.py +148 -0
- detree/opt/peephole.py +104 -0
- detree/py.typed +4 -0
- detree/spec/__init__.py +15 -0
- detree/spec/errors.py +129 -0
- detree/spec/isa.py +430 -0
- detree/spec/module.py +550 -0
- detree/spec/opinfo.py +116 -0
- detree/spec/validate.py +359 -0
- detree/vm/__init__.py +13 -0
- detree/vm/python.py +1076 -0
- detree_vm-0.1.0.dist-info/METADATA +75 -0
- detree_vm-0.1.0.dist-info/RECORD +49 -0
- detree_vm-0.1.0.dist-info/WHEEL +5 -0
- 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
|
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
|
+
]
|