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