zitcompiler 0.1.0__tar.gz
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.
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zitcompiler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Zig zust-in-time compiler for Python
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: ziglang
|
|
7
|
+
Provides-Extra: benchmarks
|
|
8
|
+
Requires-Dist: pytest-benchmark; extra == 'benchmarks'
|
|
9
|
+
Requires-Dist: rich; extra == 'benchmarks'
|
|
10
|
+
Provides-Extra: build
|
|
11
|
+
Requires-Dist: hatchling; extra == 'build'
|
|
12
|
+
Provides-Extra: cli
|
|
13
|
+
Requires-Dist: click; extra == 'cli'
|
|
14
|
+
Provides-Extra: tests
|
|
15
|
+
Requires-Dist: nox; extra == 'tests'
|
|
16
|
+
Requires-Dist: pytest; extra == 'tests'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# zitcompiler
|
|
20
|
+
|
|
21
|
+
JIT-like native extension compilation for Python using the Zig compiler. Write a Zig module, compile it at runtime, and load the result as a callable Python object — with optional comptime parameter injection so the same Zig source can be specialized differently on each call.
|
|
22
|
+
|
|
23
|
+
Requires the `ziglang` package (`pip install ziglang`).
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Basic: compile and load a function
|
|
28
|
+
|
|
29
|
+
Write a Zig module that exports a standard CPython `PyCFunction`:
|
|
30
|
+
|
|
31
|
+
```zig
|
|
32
|
+
// my_ext.zig
|
|
33
|
+
extern fn Py_IncRef(obj: ?*PyObject) void;
|
|
34
|
+
extern var _Py_NoneStruct: PyObject;
|
|
35
|
+
const PyObject = extern struct { ob_refcnt: i64, ob_type: ?*anyopaque };
|
|
36
|
+
|
|
37
|
+
export fn greet(self: ?*PyObject, args: ?*PyObject) callconv(.c) ?*PyObject {
|
|
38
|
+
_ = self; _ = args;
|
|
39
|
+
@import("std").debug.print("Hello from Zig!\n", .{});
|
|
40
|
+
Py_IncRef(&_Py_NoneStruct);
|
|
41
|
+
return &_Py_NoneStruct;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Load it from Python with the high-level helper:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from zitcompiler import zitcompiled
|
|
50
|
+
|
|
51
|
+
greet = zitcompiled(Path("my_ext.zig"), "greet")
|
|
52
|
+
greet() # Hello from Zig!
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Comptime parameters: specialize Zig code from Python
|
|
56
|
+
|
|
57
|
+
`ZigModuleDef` lets you inject Python dataclass values as Zig `comptime` constants. The Zig module imports them as a named module and the compiler eliminates all dead branches at build time.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from dataclasses import dataclass
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from zitcompiler import ZigModuleDef, zitcompiled
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Params:
|
|
66
|
+
multiplier: int = 7
|
|
67
|
+
|
|
68
|
+
module_def = ZigModuleDef(top_level=Params, structs=[], module_name="params")
|
|
69
|
+
get_multiplier = zitcompiled(Path("my_ext.zig"), "get_multiplier", module_def=module_def)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```zig
|
|
73
|
+
// my_ext.zig
|
|
74
|
+
const params = @import("params"); // injected at compile time
|
|
75
|
+
const PyObject = extern struct { ob_refcnt: i64, ob_type: ?*anyopaque };
|
|
76
|
+
extern fn PyLong_FromLong(v: c_long) ?*PyObject;
|
|
77
|
+
|
|
78
|
+
export fn get_multiplier(self: ?*PyObject, args: ?*PyObject) callconv(.c) ?*PyObject {
|
|
79
|
+
_ = self; _ = args;
|
|
80
|
+
return PyLong_FromLong(params.multiplier); // 7, resolved at compile time
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Structs defined in the dataclass are emitted as Zig `struct` types in the injected module, enabling comptime reflection (e.g. `@typeInfo(params.Point).@"struct".fields.len`).
|
|
85
|
+
|
|
86
|
+
### Low-level API
|
|
87
|
+
|
|
88
|
+
For finer control, use `zig_build_lib` directly and then `load_function` / `load_class`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import asyncio
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
from zitcompiler import BuildLibOptions, load_function, zig_build_lib
|
|
94
|
+
|
|
95
|
+
opts = BuildLibOptions(
|
|
96
|
+
module_path=Path("my_ext.zig"),
|
|
97
|
+
link_python=True,
|
|
98
|
+
output_path=Path("/tmp/my_ext.so"),
|
|
99
|
+
)
|
|
100
|
+
so_path = asyncio.run(zig_build_lib(opts))
|
|
101
|
+
greet = load_function(so_path, "greet")
|
|
102
|
+
greet()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`load_class` works the same way for exported `PyTypeObject` symbols.
|
|
106
|
+
|
|
107
|
+
## Zetaclasses
|
|
108
|
+
|
|
109
|
+
`zetaclass` is a drop-in replacement for `@dataclass` that compiles the class to a native Zig struct at decoration time. The resulting type behaves like a regular Python class but `__init__` and `__eq__` run as compiled C slots — no Python interpreter overhead.
|
|
110
|
+
|
|
111
|
+
### Usage
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from zitcompiler.zetaclasses import zetaclass
|
|
115
|
+
|
|
116
|
+
@zetaclass
|
|
117
|
+
class Point:
|
|
118
|
+
x: int
|
|
119
|
+
y: int
|
|
120
|
+
|
|
121
|
+
p1 = Point(1, 2)
|
|
122
|
+
p2 = Point(x=1, y=2)
|
|
123
|
+
assert p1 == p2
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Supported field types: `int` (`i64`), `float` (`f64`), `str` (Python string object).
|
|
127
|
+
|
|
128
|
+
Default values work the same as with `@dataclass`:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
@zetaclass
|
|
132
|
+
class Config:
|
|
133
|
+
host: str = "localhost"
|
|
134
|
+
port: int = 8080
|
|
135
|
+
timeout: float = 30.0
|
|
136
|
+
|
|
137
|
+
cfg = Config() # all defaults
|
|
138
|
+
cfg2 = Config(port=9090) # keyword override
|
|
139
|
+
assert cfg != cfg2
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Positional, keyword, and mixed argument styles are all supported:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
Config("example.com", 443) # positional
|
|
146
|
+
Config(host="example.com") # keyword only
|
|
147
|
+
Config("example.com", timeout=5) # mixed
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Attribute access works normally — fields are readable and writable:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
print(cfg.host) # "localhost"
|
|
154
|
+
cfg.port = 9090
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Current limitations
|
|
158
|
+
|
|
159
|
+
- Supported field types: `int`, `float`, `str`. Other types raise `TypeError` at decoration time.
|
|
160
|
+
- `__repr__`, `__hash__`, ordering operators, and `frozen` are not yet implemented.
|
|
161
|
+
- Compilation runs synchronously at decoration time (same as other `zitcompiler` calls).
|
|
162
|
+
|
|
163
|
+
### Internal implementation
|
|
164
|
+
|
|
165
|
+
When `@zetaclass` is applied, the decorator:
|
|
166
|
+
|
|
167
|
+
1. Reads field names, types, and default values from the class annotations (following the MRO).
|
|
168
|
+
2. Generates a `params.zig` source file containing an `extern struct` with `ob_base: PyObject` as its first field — the layout CPython requires for all heap-allocated objects — followed by one field per annotation.
|
|
169
|
+
3. Generates a `Defaults` struct holding comptime constants for each field that has a default value. String defaults are stored as `[:0]const u8` (null-terminated slice); numeric defaults as `i64`/`f64`.
|
|
170
|
+
4. Compiles `params.zig` against `core.zig` (the static Zig library shipped with the package) using `zig build-lib`.
|
|
171
|
+
5. Loads the exported `PyTypeObject` symbol via `load_class`, which calls `PyType_Ready` to finalise the type.
|
|
172
|
+
|
|
173
|
+
`core.zig` provides the comptime slot generators:
|
|
174
|
+
|
|
175
|
+
| Slot | Generator | What it does |
|
|
176
|
+
|------|-----------|--------------|
|
|
177
|
+
| `tp_init` | `initFn(T, Defaults)` | Iterates struct fields at comptime; reads positional args, then kwargs, then comptime defaults. Stores values with ref-counting for `?*PyObject` fields. |
|
|
178
|
+
| `tp_richcompare` | `richCompareFn(T)` | Field-by-field equality via `!=` for numeric fields and `PyObject_RichCompareBool` for string fields. Returns `Py_NotImplemented` for non-EQ/NE ops or mismatched types. |
|
|
179
|
+
| `tp_members` | `membersArray(T)` | Comptime-generates a null-terminated `PyMemberDef[]` using `@offsetOf` for each field. Python's built-in member descriptor machinery handles all get/set at runtime — no Zig code runs on attribute access. |
|
|
180
|
+
| `tp_dealloc` | `deallocFn(T)` | Decrefs all `?*PyObject` fields then calls `PyObject_Free`. Only wired in when the struct contains object fields; otherwise null and CPython inherits the default from `object`. |
|
|
181
|
+
|
|
182
|
+
The `Defaults` struct approach avoids any Python-level wrapper class: defaults are resolved entirely in the compiled `tp_init` slot, so the loaded `PyTypeObject` is the final Python type with no subclassing or runtime indirection.
|
|
183
|
+
|
|
184
|
+
## JSON schema → native class
|
|
185
|
+
|
|
186
|
+
`zitcompiler.jsonschemas` can compile a native class directly from a JSON schema definition, without writing any Python class by hand.
|
|
187
|
+
|
|
188
|
+
### Quick start
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import json
|
|
192
|
+
from pathlib import Path
|
|
193
|
+
from zitcompiler.jsonschemas import zetaify
|
|
194
|
+
|
|
195
|
+
schema = json.loads(Path("person.json").read_text())
|
|
196
|
+
Person = zetaify(schema)
|
|
197
|
+
|
|
198
|
+
p = Person(name="Alice", age=30)
|
|
199
|
+
print(p) # Person(name='Alice', age=30)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`zetaify` accepts a raw JSON-decoded dict or a pre-parsed `SchemaDef`. It returns a fully compiled `@zetaclass` type.
|
|
203
|
+
|
|
204
|
+
### Saving a type stub
|
|
205
|
+
|
|
206
|
+
Pass `stub_path` to write a `.pyi` file alongside your schema. The stub includes the `@zetaclass` decorator so it reads as a valid, annotated class definition:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
Person = zetaify(schema, stub_path=Path("person.pyi"))
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Generated `person.pyi`:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from zitcompiler.zetaclasses import zetaclass
|
|
216
|
+
|
|
217
|
+
@zetaclass
|
|
218
|
+
class Person:
|
|
219
|
+
name: str
|
|
220
|
+
age: int
|
|
221
|
+
height: float | None
|
|
222
|
+
active: bool | None
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Forwarding zetaclass options
|
|
226
|
+
|
|
227
|
+
Any keyword accepted by `@zetaclass` can be forwarded through `zetaify`:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
Person = zetaify(schema, frozen=True, kw_only=True)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Lower-level API
|
|
234
|
+
|
|
235
|
+
Use `parse_schema` and `generate_stub` directly if you need the intermediate representations:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from zitcompiler.jsonschemas import generate_stub, parse_schema
|
|
239
|
+
|
|
240
|
+
schema_def = parse_schema(raw) # → SchemaDef
|
|
241
|
+
print(generate_stub(schema_def)) # plain class body
|
|
242
|
+
print(generate_stub(schema_def, with_decorator=True)) # with @zetaclass header
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Current limitations
|
|
246
|
+
|
|
247
|
+
- Only flat (non-nested) schemas are supported. Nested `object` properties are not yet handled.
|
|
248
|
+
- Optional fields (`T | None`, i.e. properties absent from `required`) are present in the written stub but **excluded from the compiled native struct**, since the Zig backend has no nullable scalar type mapping yet. Only required fields with types `string`, `integer`, or `number` become native struct fields.
|
|
249
|
+
- `boolean` and `null` JSON schema types appear in stubs as `bool` / `None` but are not currently supported as zetaclass field types; including them as required fields will raise a `TypeError` at compilation time.
|
|
250
|
+
|
|
251
|
+
## Tests
|
|
252
|
+
|
|
253
|
+
### Unit tests
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
uv run pytest tests/
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Covers Zig module compilation (`zig_build_lib`), comptime parameter injection, and `load_function` / `load_class`.
|
|
260
|
+
|
|
261
|
+
### Build backend integration tests
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
nox -f tests/build_backend/noxfile.py
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Tests the hatch build hook end-to-end. The session:
|
|
268
|
+
|
|
269
|
+
1. Installs `hatchling` and a local editable copy of `zitcompiler` into a fresh virtual environment.
|
|
270
|
+
2. Builds the `greetings` test package (`tests/build_backend/test_pkg/`) with `uv build --no-build-isolation`. During the wheel build, the hatch hook imports `greetings`, which triggers a `zitcompiled()` call, and the resulting `.so` is bundled into the wheel.
|
|
271
|
+
3. Installs the built wheel and runs `pytest tests/build_backend/test_aot.py`, which verifies that the pre-compiled `.so` is present inside the installed package and that all exported symbols (function and zetaclasses) work correctly.
|
|
272
|
+
|
|
273
|
+
The test package (`greetings`) contains one `zitcompiled()` Zig function (`hello_world`) and three `@zetaclass` types (`Greeter`, `Point`, `Color`). The `hello_world` function is the AoT target; zetaclasses compile at import time as usual.
|
|
274
|
+
|
|
275
|
+
## Known limitations
|
|
276
|
+
|
|
277
|
+
### Incremental compilation on Linux (ELF targets)
|
|
278
|
+
|
|
279
|
+
`BuildLibOptions.incremental = True` passes `-fincremental` to the Zig compiler, which activates the `elf2` linker backend. As of Zig 0.16, **elf2 does not implement saving linker state to disk**, so incremental `build-lib` always fails on ELF targets with:
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
error(compilation): TODO implement saving linker state for elf2
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Why this exists:** true incremental linking requires persisting the linker's internal data structures (symbol tables, section allocations, relocation records, virtual address assignments) between builds so subsequent builds can restore and patch only what changed. The elf2 linker tracks dirty sections in memory via a `ZigObject` structure but cannot yet serialize that state to disk.
|
|
286
|
+
|
|
287
|
+
**This is independent of what you link against** — the error occurs even for a minimal Zig module with no external dependencies.
|
|
288
|
+
|
|
289
|
+
**Roadmap:** tracked in [ziglang/zig#21165](https://github.com/ziglang/zig/issues/21165). Incremental compilation works today for pure Zig executables and (as of April 2026) the LLVM backend; `build-lib` on ELF is the remaining gap. Once Zig lands linker state serialization, `incremental = True` will work transparently.
|
|
290
|
+
|
|
291
|
+
`zitcompiler` emits a `logging.WARNING` when `incremental = True` is requested on a non-Windows, non-macOS platform.
|
|
292
|
+
|
|
293
|
+
## Examples
|
|
294
|
+
|
|
295
|
+
### hello_world
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
zig build-lib examples/hello_world.zig
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### hello_world_ext (Python C extension, module-level function)
|
|
302
|
+
|
|
303
|
+
```sh
|
|
304
|
+
zig build-lib -dynamic -lc examples/hello_world_ext.zig -femit-bin=hello_world.so $(python3-config --ldflags --embed)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
import sys; sys.path.insert(0, ".")
|
|
309
|
+
import hello_world
|
|
310
|
+
hello_world.hello_world() # prints: Hello from zig!
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### greeter_ext (Python C extension, class with method)
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
zig build-lib -dynamic -lc examples/greeter_ext.zig -femit-bin=greeter.so $(python3-config --ldflags --embed)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
import sys; sys.path.insert(0, ".")
|
|
321
|
+
import greeter
|
|
322
|
+
g = greeter.Greeter()
|
|
323
|
+
g.hello_world() # prints: Hello from zig!
|
|
324
|
+
```
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# zitcompiler
|
|
2
|
+
|
|
3
|
+
JIT-like native extension compilation for Python using the Zig compiler. Write a Zig module, compile it at runtime, and load the result as a callable Python object — with optional comptime parameter injection so the same Zig source can be specialized differently on each call.
|
|
4
|
+
|
|
5
|
+
Requires the `ziglang` package (`pip install ziglang`).
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### Basic: compile and load a function
|
|
10
|
+
|
|
11
|
+
Write a Zig module that exports a standard CPython `PyCFunction`:
|
|
12
|
+
|
|
13
|
+
```zig
|
|
14
|
+
// my_ext.zig
|
|
15
|
+
extern fn Py_IncRef(obj: ?*PyObject) void;
|
|
16
|
+
extern var _Py_NoneStruct: PyObject;
|
|
17
|
+
const PyObject = extern struct { ob_refcnt: i64, ob_type: ?*anyopaque };
|
|
18
|
+
|
|
19
|
+
export fn greet(self: ?*PyObject, args: ?*PyObject) callconv(.c) ?*PyObject {
|
|
20
|
+
_ = self; _ = args;
|
|
21
|
+
@import("std").debug.print("Hello from Zig!\n", .{});
|
|
22
|
+
Py_IncRef(&_Py_NoneStruct);
|
|
23
|
+
return &_Py_NoneStruct;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Load it from Python with the high-level helper:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from zitcompiler import zitcompiled
|
|
32
|
+
|
|
33
|
+
greet = zitcompiled(Path("my_ext.zig"), "greet")
|
|
34
|
+
greet() # Hello from Zig!
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Comptime parameters: specialize Zig code from Python
|
|
38
|
+
|
|
39
|
+
`ZigModuleDef` lets you inject Python dataclass values as Zig `comptime` constants. The Zig module imports them as a named module and the compiler eliminates all dead branches at build time.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from zitcompiler import ZigModuleDef, zitcompiled
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Params:
|
|
48
|
+
multiplier: int = 7
|
|
49
|
+
|
|
50
|
+
module_def = ZigModuleDef(top_level=Params, structs=[], module_name="params")
|
|
51
|
+
get_multiplier = zitcompiled(Path("my_ext.zig"), "get_multiplier", module_def=module_def)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```zig
|
|
55
|
+
// my_ext.zig
|
|
56
|
+
const params = @import("params"); // injected at compile time
|
|
57
|
+
const PyObject = extern struct { ob_refcnt: i64, ob_type: ?*anyopaque };
|
|
58
|
+
extern fn PyLong_FromLong(v: c_long) ?*PyObject;
|
|
59
|
+
|
|
60
|
+
export fn get_multiplier(self: ?*PyObject, args: ?*PyObject) callconv(.c) ?*PyObject {
|
|
61
|
+
_ = self; _ = args;
|
|
62
|
+
return PyLong_FromLong(params.multiplier); // 7, resolved at compile time
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Structs defined in the dataclass are emitted as Zig `struct` types in the injected module, enabling comptime reflection (e.g. `@typeInfo(params.Point).@"struct".fields.len`).
|
|
67
|
+
|
|
68
|
+
### Low-level API
|
|
69
|
+
|
|
70
|
+
For finer control, use `zig_build_lib` directly and then `load_function` / `load_class`:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
from zitcompiler import BuildLibOptions, load_function, zig_build_lib
|
|
76
|
+
|
|
77
|
+
opts = BuildLibOptions(
|
|
78
|
+
module_path=Path("my_ext.zig"),
|
|
79
|
+
link_python=True,
|
|
80
|
+
output_path=Path("/tmp/my_ext.so"),
|
|
81
|
+
)
|
|
82
|
+
so_path = asyncio.run(zig_build_lib(opts))
|
|
83
|
+
greet = load_function(so_path, "greet")
|
|
84
|
+
greet()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`load_class` works the same way for exported `PyTypeObject` symbols.
|
|
88
|
+
|
|
89
|
+
## Zetaclasses
|
|
90
|
+
|
|
91
|
+
`zetaclass` is a drop-in replacement for `@dataclass` that compiles the class to a native Zig struct at decoration time. The resulting type behaves like a regular Python class but `__init__` and `__eq__` run as compiled C slots — no Python interpreter overhead.
|
|
92
|
+
|
|
93
|
+
### Usage
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from zitcompiler.zetaclasses import zetaclass
|
|
97
|
+
|
|
98
|
+
@zetaclass
|
|
99
|
+
class Point:
|
|
100
|
+
x: int
|
|
101
|
+
y: int
|
|
102
|
+
|
|
103
|
+
p1 = Point(1, 2)
|
|
104
|
+
p2 = Point(x=1, y=2)
|
|
105
|
+
assert p1 == p2
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Supported field types: `int` (`i64`), `float` (`f64`), `str` (Python string object).
|
|
109
|
+
|
|
110
|
+
Default values work the same as with `@dataclass`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
@zetaclass
|
|
114
|
+
class Config:
|
|
115
|
+
host: str = "localhost"
|
|
116
|
+
port: int = 8080
|
|
117
|
+
timeout: float = 30.0
|
|
118
|
+
|
|
119
|
+
cfg = Config() # all defaults
|
|
120
|
+
cfg2 = Config(port=9090) # keyword override
|
|
121
|
+
assert cfg != cfg2
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Positional, keyword, and mixed argument styles are all supported:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
Config("example.com", 443) # positional
|
|
128
|
+
Config(host="example.com") # keyword only
|
|
129
|
+
Config("example.com", timeout=5) # mixed
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Attribute access works normally — fields are readable and writable:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
print(cfg.host) # "localhost"
|
|
136
|
+
cfg.port = 9090
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Current limitations
|
|
140
|
+
|
|
141
|
+
- Supported field types: `int`, `float`, `str`. Other types raise `TypeError` at decoration time.
|
|
142
|
+
- `__repr__`, `__hash__`, ordering operators, and `frozen` are not yet implemented.
|
|
143
|
+
- Compilation runs synchronously at decoration time (same as other `zitcompiler` calls).
|
|
144
|
+
|
|
145
|
+
### Internal implementation
|
|
146
|
+
|
|
147
|
+
When `@zetaclass` is applied, the decorator:
|
|
148
|
+
|
|
149
|
+
1. Reads field names, types, and default values from the class annotations (following the MRO).
|
|
150
|
+
2. Generates a `params.zig` source file containing an `extern struct` with `ob_base: PyObject` as its first field — the layout CPython requires for all heap-allocated objects — followed by one field per annotation.
|
|
151
|
+
3. Generates a `Defaults` struct holding comptime constants for each field that has a default value. String defaults are stored as `[:0]const u8` (null-terminated slice); numeric defaults as `i64`/`f64`.
|
|
152
|
+
4. Compiles `params.zig` against `core.zig` (the static Zig library shipped with the package) using `zig build-lib`.
|
|
153
|
+
5. Loads the exported `PyTypeObject` symbol via `load_class`, which calls `PyType_Ready` to finalise the type.
|
|
154
|
+
|
|
155
|
+
`core.zig` provides the comptime slot generators:
|
|
156
|
+
|
|
157
|
+
| Slot | Generator | What it does |
|
|
158
|
+
|------|-----------|--------------|
|
|
159
|
+
| `tp_init` | `initFn(T, Defaults)` | Iterates struct fields at comptime; reads positional args, then kwargs, then comptime defaults. Stores values with ref-counting for `?*PyObject` fields. |
|
|
160
|
+
| `tp_richcompare` | `richCompareFn(T)` | Field-by-field equality via `!=` for numeric fields and `PyObject_RichCompareBool` for string fields. Returns `Py_NotImplemented` for non-EQ/NE ops or mismatched types. |
|
|
161
|
+
| `tp_members` | `membersArray(T)` | Comptime-generates a null-terminated `PyMemberDef[]` using `@offsetOf` for each field. Python's built-in member descriptor machinery handles all get/set at runtime — no Zig code runs on attribute access. |
|
|
162
|
+
| `tp_dealloc` | `deallocFn(T)` | Decrefs all `?*PyObject` fields then calls `PyObject_Free`. Only wired in when the struct contains object fields; otherwise null and CPython inherits the default from `object`. |
|
|
163
|
+
|
|
164
|
+
The `Defaults` struct approach avoids any Python-level wrapper class: defaults are resolved entirely in the compiled `tp_init` slot, so the loaded `PyTypeObject` is the final Python type with no subclassing or runtime indirection.
|
|
165
|
+
|
|
166
|
+
## JSON schema → native class
|
|
167
|
+
|
|
168
|
+
`zitcompiler.jsonschemas` can compile a native class directly from a JSON schema definition, without writing any Python class by hand.
|
|
169
|
+
|
|
170
|
+
### Quick start
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import json
|
|
174
|
+
from pathlib import Path
|
|
175
|
+
from zitcompiler.jsonschemas import zetaify
|
|
176
|
+
|
|
177
|
+
schema = json.loads(Path("person.json").read_text())
|
|
178
|
+
Person = zetaify(schema)
|
|
179
|
+
|
|
180
|
+
p = Person(name="Alice", age=30)
|
|
181
|
+
print(p) # Person(name='Alice', age=30)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`zetaify` accepts a raw JSON-decoded dict or a pre-parsed `SchemaDef`. It returns a fully compiled `@zetaclass` type.
|
|
185
|
+
|
|
186
|
+
### Saving a type stub
|
|
187
|
+
|
|
188
|
+
Pass `stub_path` to write a `.pyi` file alongside your schema. The stub includes the `@zetaclass` decorator so it reads as a valid, annotated class definition:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
Person = zetaify(schema, stub_path=Path("person.pyi"))
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Generated `person.pyi`:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from zitcompiler.zetaclasses import zetaclass
|
|
198
|
+
|
|
199
|
+
@zetaclass
|
|
200
|
+
class Person:
|
|
201
|
+
name: str
|
|
202
|
+
age: int
|
|
203
|
+
height: float | None
|
|
204
|
+
active: bool | None
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Forwarding zetaclass options
|
|
208
|
+
|
|
209
|
+
Any keyword accepted by `@zetaclass` can be forwarded through `zetaify`:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
Person = zetaify(schema, frozen=True, kw_only=True)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Lower-level API
|
|
216
|
+
|
|
217
|
+
Use `parse_schema` and `generate_stub` directly if you need the intermediate representations:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from zitcompiler.jsonschemas import generate_stub, parse_schema
|
|
221
|
+
|
|
222
|
+
schema_def = parse_schema(raw) # → SchemaDef
|
|
223
|
+
print(generate_stub(schema_def)) # plain class body
|
|
224
|
+
print(generate_stub(schema_def, with_decorator=True)) # with @zetaclass header
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Current limitations
|
|
228
|
+
|
|
229
|
+
- Only flat (non-nested) schemas are supported. Nested `object` properties are not yet handled.
|
|
230
|
+
- Optional fields (`T | None`, i.e. properties absent from `required`) are present in the written stub but **excluded from the compiled native struct**, since the Zig backend has no nullable scalar type mapping yet. Only required fields with types `string`, `integer`, or `number` become native struct fields.
|
|
231
|
+
- `boolean` and `null` JSON schema types appear in stubs as `bool` / `None` but are not currently supported as zetaclass field types; including them as required fields will raise a `TypeError` at compilation time.
|
|
232
|
+
|
|
233
|
+
## Tests
|
|
234
|
+
|
|
235
|
+
### Unit tests
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
uv run pytest tests/
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Covers Zig module compilation (`zig_build_lib`), comptime parameter injection, and `load_function` / `load_class`.
|
|
242
|
+
|
|
243
|
+
### Build backend integration tests
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
nox -f tests/build_backend/noxfile.py
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Tests the hatch build hook end-to-end. The session:
|
|
250
|
+
|
|
251
|
+
1. Installs `hatchling` and a local editable copy of `zitcompiler` into a fresh virtual environment.
|
|
252
|
+
2. Builds the `greetings` test package (`tests/build_backend/test_pkg/`) with `uv build --no-build-isolation`. During the wheel build, the hatch hook imports `greetings`, which triggers a `zitcompiled()` call, and the resulting `.so` is bundled into the wheel.
|
|
253
|
+
3. Installs the built wheel and runs `pytest tests/build_backend/test_aot.py`, which verifies that the pre-compiled `.so` is present inside the installed package and that all exported symbols (function and zetaclasses) work correctly.
|
|
254
|
+
|
|
255
|
+
The test package (`greetings`) contains one `zitcompiled()` Zig function (`hello_world`) and three `@zetaclass` types (`Greeter`, `Point`, `Color`). The `hello_world` function is the AoT target; zetaclasses compile at import time as usual.
|
|
256
|
+
|
|
257
|
+
## Known limitations
|
|
258
|
+
|
|
259
|
+
### Incremental compilation on Linux (ELF targets)
|
|
260
|
+
|
|
261
|
+
`BuildLibOptions.incremental = True` passes `-fincremental` to the Zig compiler, which activates the `elf2` linker backend. As of Zig 0.16, **elf2 does not implement saving linker state to disk**, so incremental `build-lib` always fails on ELF targets with:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
error(compilation): TODO implement saving linker state for elf2
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Why this exists:** true incremental linking requires persisting the linker's internal data structures (symbol tables, section allocations, relocation records, virtual address assignments) between builds so subsequent builds can restore and patch only what changed. The elf2 linker tracks dirty sections in memory via a `ZigObject` structure but cannot yet serialize that state to disk.
|
|
268
|
+
|
|
269
|
+
**This is independent of what you link against** — the error occurs even for a minimal Zig module with no external dependencies.
|
|
270
|
+
|
|
271
|
+
**Roadmap:** tracked in [ziglang/zig#21165](https://github.com/ziglang/zig/issues/21165). Incremental compilation works today for pure Zig executables and (as of April 2026) the LLVM backend; `build-lib` on ELF is the remaining gap. Once Zig lands linker state serialization, `incremental = True` will work transparently.
|
|
272
|
+
|
|
273
|
+
`zitcompiler` emits a `logging.WARNING` when `incremental = True` is requested on a non-Windows, non-macOS platform.
|
|
274
|
+
|
|
275
|
+
## Examples
|
|
276
|
+
|
|
277
|
+
### hello_world
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
zig build-lib examples/hello_world.zig
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### hello_world_ext (Python C extension, module-level function)
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
zig build-lib -dynamic -lc examples/hello_world_ext.zig -femit-bin=hello_world.so $(python3-config --ldflags --embed)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
import sys; sys.path.insert(0, ".")
|
|
291
|
+
import hello_world
|
|
292
|
+
hello_world.hello_world() # prints: Hello from zig!
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### greeter_ext (Python C extension, class with method)
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
zig build-lib -dynamic -lc examples/greeter_ext.zig -femit-bin=greeter.so $(python3-config --ldflags --embed)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
import sys; sys.path.insert(0, ".")
|
|
303
|
+
import greeter
|
|
304
|
+
g = greeter.Greeter()
|
|
305
|
+
g.hello_world() # prints: Hello from zig!
|
|
306
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zitcompiler"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A Zig zust-in-time compiler for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = ["ziglang"]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
cli = ["click"]
|
|
15
|
+
tests = ["pytest", "nox"]
|
|
16
|
+
benchmarks = ["pytest-benchmark", "rich"]
|
|
17
|
+
build = ["hatchling"]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
zit = "zitcompiler.cli:zit"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.sdist]
|
|
23
|
+
include = ["src/zitcompiler/zetaclasses/*.zig"]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
force-include = { "src/zitcompiler/zetaclasses/core.zig" = "zitcompiler/zetaclasses/core.zig" }
|
|
27
|
+
|
|
28
|
+
[project.entry-points.hatch]
|
|
29
|
+
zitcompiler = "zitcompiler.build_backend.hatch"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version]
|
|
32
|
+
source = "vcs"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.hooks.vcs]
|
|
35
|
+
version-file = "src/zitcompiler/_version.py"
|
|
@@ -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 = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|