yjson 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,7 @@
1
+ include build.sh
2
+ include THIRD_PARTY_NOTICES.md
3
+ include src/mojson.pyi
4
+ recursive-include src *.mojo *.c *.py *.pyi
5
+ recursive-include tests *.py
6
+ recursive-include examples *.py
7
+ global-exclude __pycache__ *.py[cod] *.so *.o
yjson-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: yjson
3
+ Version: 0.1.0
4
+ Summary: JSON serializer for CPython written in Mojo, with an orjson-style API
5
+ Author: Farhan Ali Raza
6
+ Project-URL: Homepage, https://github.com/FarhanAliRaza/mojson
7
+ Project-URL: Repository, https://github.com/FarhanAliRaza/mojson
8
+ Project-URL: Issues, https://github.com/FarhanAliRaza/mojson/issues
9
+ Project-URL: Changelog, https://github.com/FarhanAliRaza/mojson/releases
10
+ Keywords: json,orjson,mojo,serialization,numpy,simd
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
21
+ Classifier: Programming Language :: Python :: Implementation :: CPython
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: <3.16,>=3.11
25
+ Description-Content-Type: text/markdown
26
+
27
+ # mojson
28
+
29
+ A JSON serializer for CPython written in Mojo, with an orjson-style API.
30
+ NaN / Infinity are written the way Python's `json` writes them
31
+ (`NaN`, `Infinity`, `-Infinity`), and NumPy arrays and scalars are supported directly.
32
+
33
+ ## Install
34
+
35
+ The package is published on PyPI as `yjson` (the name `mojson` is too close to an
36
+ existing project); the module you import is still `mojson`. Wheels are built for
37
+ CPython 3.11 - 3.15 on x86-64 Linux
38
+ (`manylinux_2_35`, i.e. glibc 2.35+ such as Ubuntu 22.04 or newer) and need a CPU
39
+ with AVX2. They bundle the Mojo runtime library, so nothing else is required:
40
+
41
+ ```bash
42
+ pip install yjson # or: uv add yjson
43
+ python -c 'import mojson; print(mojson.dumps({"ok": True}))'
44
+ ```
45
+
46
+ Building from the sdist needs the Mojo compiler on `PATH` (see below).
47
+
48
+ ## Build
49
+
50
+ Requirements: CPython 3.11, 3.12, 3.13, 3.14 or 3.15 (default GIL builds) with headers,
51
+ a C compiler, x86-64 with AVX2, and Mojo 1.1 (`pip install mojo`). The build
52
+ targets one interpreter at a time: it uses `.venv-bench/bin/python` if present,
53
+ otherwise `python3`; override with `PYTHON=...`.
54
+
55
+ ```bash
56
+ ./build.sh # -> build/mojson.cpython-312-x86_64-linux-gnu.so
57
+ PYTHON=python3.14 ./build.sh # -> build/mojson.cpython-314-x86_64-linux-gnu.so
58
+ export PYTHONPATH="$PWD/build${PYTHONPATH:+:$PYTHONPATH}"
59
+ ```
60
+
61
+ The output carries the interpreter's extension suffix, so builds for several
62
+ versions coexist in `build/` and each interpreter imports its own. The Mojo
63
+ loops read CPython object fields directly; `build.sh` probes those offsets from
64
+ the target headers (`src/layout_probe.c`) and passes them to both compilers.
65
+ The C shim re-checks them with `_Static_assert`, and the module verifies them
66
+ against live objects at import, so an unsupported interpreter fails with an
67
+ `ImportError` instead of reading memory wrongly. The layout differences in this
68
+ range are small: 3.11 has a longer str header (the legacy `wstr` field) and
69
+ keeps an int's sign and digit count in `ob_size`, for which the build selects a
70
+ tag-synthesizing variant that leaves the 3.12+ code untouched; 3.14 moved tuple
71
+ items by adding a cached tuple hash.
72
+
73
+ ## Use
74
+
75
+ ```python
76
+ import mojson
77
+ mojson.dumps({"a": [1, 2.5, None], "b": float("nan")}) # b'{"a":[1,2.5,null],"b":NaN}'
78
+ mojson.dumps(obj).decode() # if you need a str
79
+ mojson.loads(b'{"a": 1}') # -> {"a": 1}
80
+ ```
81
+
82
+ See `examples/usage.py` (NumPy, NaN, files, errors).
83
+
84
+ `dumps(obj, /, default=None, option=None)` returns bytes. In addition to the
85
+ basic JSON types, it supports datetime/date/time, UUID, Enum, dataclass
86
+ instances, and subclasses of str/int/list/dict. Dataclass fields beginning
87
+ with `_` are omitted. Tuple subclasses use `default` rather than being treated
88
+ as arrays. Encoding errors are `JSONEncodeError`, an alias of `TypeError`;
89
+ exceptions raised by a default callback are attached as `__cause__`.
90
+
91
+ ```python
92
+ import datetime
93
+ import decimal
94
+ import mojson
95
+
96
+ data = {"created": datetime.datetime(2024, 1, 2), "amount": decimal.Decimal("12.50")}
97
+ out = mojson.dumps(
98
+ data,
99
+ default=str,
100
+ option=mojson.OPT_NAIVE_UTC | mojson.OPT_UTC_Z | mojson.OPT_INDENT_2,
101
+ )
102
+ mojson.dumps({2: "b", 1: "a"}, option=mojson.OPT_NON_STR_KEYS | mojson.OPT_SORT_KEYS)
103
+ mojson.dumps({"cached": mojson.Fragment(b'{"a":1}')})
104
+ ```
105
+
106
+ Options use the same bit values as [orjson](https://github.com/ijl/orjson#option):
107
+ `OPT_INDENT_2`, `OPT_SORT_KEYS`, `OPT_NON_STR_KEYS`, `OPT_STRICT_INTEGER`,
108
+ `OPT_APPEND_NEWLINE`, `OPT_NAIVE_UTC`, `OPT_UTC_Z`, `OPT_OMIT_MICROSECONDS`,
109
+ and `OPT_PASSTHROUGH_DATACLASS`, `OPT_PASSTHROUGH_DATETIME`,
110
+ `OPT_PASSTHROUGH_SUBCLASS`. `OPT_SERIALIZE_NUMPY` is accepted; NumPy support
111
+ is already automatic. The deprecated `OPT_SERIALIZE_DATACLASS` and
112
+ `OPT_SERIALIZE_UUID` are zero. Flags can be combined with `|`.
113
+
114
+ Integers support the range `-2**63` through `2**64 - 1`; strict mode restricts
115
+ values to `-(2**53 - 1)` through `2**53 - 1`. Non-string integer keys retain
116
+ the 64-bit range in strict mode. Non-string key conversion preserves duplicate
117
+ JSON keys. Fragments insert their contents verbatim, including under indentation;
118
+ validate the contents yourself when needed.
119
+
120
+ `OPT_INDENT_2`, `OPT_SORT_KEYS` and `OPT_NON_STR_KEYS` run on the same
121
+ compiled writers as the compact default (indentation is a compile-time variant
122
+ of those loops; sorting and non-str keys snapshot the dict as native records),
123
+ so they stay close to orjson's speed for the same option. `OPT_STRICT_INTEGER`
124
+ and `OPT_PASSTHROUGH_SUBCLASS` use a generic traversal that checks every value
125
+ and is slower. Custom conversions and uncommon types cost additional work;
126
+ measure their speed on your payload (`bench/bench_shapes.py`). `loads` uses Python's standard parser with UTF-8, nonfinite-number,
127
+ and surrogate checks. It accepts str/bytes/bytearray/contiguous memoryview and
128
+ raises `JSONDecodeError` (a subclass of `json.JSONDecodeError`). Its parsing
129
+ speed and maximum nesting follow the stdlib backend, rather than orjson's parser.
130
+
131
+ `dumps_socket(obj, /, default=None)` is a separate native encoder for Reflex
132
+ wire packets. It accepts arbitrary-size integers (subject to CPython's decimal
133
+ digit limit), preserves NaN/Infinity and `None`, escapes lone surrogates, and
134
+ protects strings that collide with Reflex's special-value markers. It returns
135
+ bytes and accepts no formatting options. Framework custom types use `default`.
136
+ The integration in `bench/reflex_codec.py` replaces the socket boundary with
137
+ this function, eliminating stdlib retries and repeated Python container walks.
138
+ Ordinary `dumps()` keeps its integer range and UTF-8 error behavior.
139
+
140
+ ## Test and benchmark
141
+
142
+ ```bash
143
+ pip install orjson numpy
144
+ python tests/check_correctness.py path/to/jsonexamples # corpus optional
145
+ python tests/check_features.py # options, types, callbacks, decoding
146
+ python bench/bench_paired.py path/to/jsonexamples # mojson vs orjson, robust
147
+ python bench/bench_features.py # enabled feature paths vs orjson
148
+ python bench/bench_shapes.py --cpu 2 # per payload shape vs orjson (where it wins or loses)
149
+ python bench/bench_regression.py # requires a baseline build in build/baseline/
150
+ python bench/bench_all_libraries.py path/to/jsonexamples # + msgspec, ujson, rapidjson, json, simplejson
151
+ python bench/bench_numpy.py
152
+ ```
153
+
154
+ Benchmark corpus: `jsonexamples/` from https://github.com/simdjson/simdjson-data.
155
+
156
+ The [Reflex PR 6116 comparison](bench/REFLEX.md) documents a pinned framework
157
+ checkout, unchanged upstream codec tests, paired encode/event benchmarks, and
158
+ real browser checks for both dump backends. Published measurements and validation
159
+ are in [bench/results](bench/results/README.md), including the general 14-file
160
+ comparison, native socket benchmarks and the default-path regression check.
161
+ The same directory holds the [CPython 3.11 - 3.15 measurements](bench/results/README.md#cpython-311---315-version-port)
162
+ and the [profile-driven improvements](bench/results/README.md#profile-driven-encoder-improvements-cpython-314)
163
+ measured on 3.14 (corpus 1.18× faster than before them; 1.28–1.32× of orjson on every interpreter).
164
+ The [all-events Reflex run](bench/results/README.md#reflex-pr-6116-every-event-workload)
165
+ measures every workload of the PR's event benchmark: all at parity, because
166
+ those deltas spend 88–97% of their encode time in Reflex's Python `default()`
167
+ serializer for pydantic models, which neither codec can skip.
168
+ The orjson baseline retains the PR's original
169
+ codec; mojson replaces its socket retry logic with native serialization.
170
+ Earlier option and indentation results remain in `build/fix-options.json`
171
+ and `build/indent-reflex-benchmark.json`.
172
+
173
+ ### Packaging and CI
174
+
175
+ `pyproject.toml` describes the package; `setup.py` only teaches setuptools to
176
+ compile the extension through `build.sh`, so `uv build` and `pip install .`
177
+ work with the Mojo compiler on `PATH`. The dependency groups `test`, `wheel`
178
+ and `mojo` are pinned in `uv.lock`:
179
+
180
+ ```bash
181
+ UV_PROJECT_ENVIRONMENT=.venv-mojo uv sync --only-group mojo --python 3.13 # Mojo 1.1
182
+ export PATH="$PWD/.venv-mojo/bin:$PATH"
183
+ uv build --sdist
184
+ uv build --wheel --python 3.13 --out-dir dist dist/yjson-*.tar.gz # cp313 wheel
185
+ uv sync --only-group wheel # auditwheel + patchelf into .venv
186
+ PATH="$PWD/.venv/bin:$PATH" auditwheel repair \
187
+ --ldpaths "$(.venv-mojo/bin/python -c 'import modular; print(modular.__path__[0] + "/lib")')" \
188
+ --disable-isa-ext-check --wheel-dir wheelhouse dist/*.whl
189
+ ```
190
+
191
+ `auditwheel repair` copies `libKGENCompilerRTShared.so` and its dependencies
192
+ into `mojson.libs/` and sets the `manylinux_2_35` tag (the floor set by the Mojo
193
+ runtime). `--disable-isa-ext-check` is required because the extension targets
194
+ x86-64-v3 (AVX2) by design.
195
+
196
+ [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs the same steps on
197
+ every push and pull request: it builds the sdist, builds one wheel per
198
+ interpreter from that sdist, repairs it, installs it into a clean environment
199
+ and runs `tests/check_features.py`, `tests/check_correctness.py` (with the
200
+ simdjson corpus) and `examples/usage.py` against the installed wheel. Pushing a
201
+ tag `vX.Y.Z` whose version matches `pyproject.toml` additionally publishes the
202
+ sdist and wheels to PyPI through [trusted publishing](https://docs.pypi.org/trusted-publishers/)
203
+ from the `pypi` GitHub environment, so no API token is stored. To release:
204
+ bump `version` in `pyproject.toml`, commit, then `git tag v0.1.0 && git push origin v0.1.0`.
205
+
206
+ ### Local comparison with uv
207
+
208
+ If Mojo is already installed in `.venv`, keep that compiler environment and use
209
+ a separate CPython environment for the extension (3.12 shown; any of 3.11 - 3.15 works):
210
+
211
+ ```bash
212
+ uv venv --python 3.12 .venv-bench
213
+ uv pip install --python .venv-bench/bin/python orjson numpy
214
+ PATH="$PWD/.venv/bin:$PATH" ./build.sh
215
+ .venv-bench/bin/python tools/fetch_corpus.py
216
+ .venv-bench/bin/python tests/check_correctness.py build/jsonexamples
217
+ .venv-bench/bin/python tests/check_features.py
218
+ .venv-bench/bin/python bench/bench_paired.py build/jsonexamples --cpu 2 --micro --output build/paired-local.json
219
+ ```
220
+
221
+ To build and test every supported version side by side:
222
+
223
+ ```bash
224
+ for v in 3.11 3.13 3.14 3.15; do
225
+ uv venv --python $v .venv-py$v && uv pip install --python .venv-py$v/bin/python orjson numpy
226
+ PATH="$PWD/.venv/bin:$PATH" PYTHON=.venv-py$v/bin/python ./build.sh
227
+ .venv-py$v/bin/python tests/check_correctness.py build/jsonexamples
228
+ .venv-py$v/bin/python tests/check_features.py
229
+ done
230
+ ```
231
+
232
+ Choose an available logical CPU for `--cpu`, or omit it. The paired benchmark
233
+ checks output equality, warms both serializers, calibrates a common batch size
234
+ to at least 10 ms, and alternates order across 40 pairs. It reports median time
235
+ per call, the median `orjson time / mojson time` ratio, and ratio quartiles.
236
+ Ratios above 1 mean mojson is faster. The optional JSON report includes every
237
+ sample and environment metadata. The corpus downloader records the source
238
+ revision and SHA-256 hashes in `build/jsonexamples/manifest.json.txt`.
239
+
240
+ ## What's inside (src/mojson.mojo)
241
+
242
+ - Direct reads of CPython object layouts (type pointer, list/tuple items, compact ints, float bits, str data)
243
+ for 3.11 - 3.15, with the offsets supplied by the build and verified at import,
244
+ and `external_call` into the CPython C API; output written straight into a `bytes` object.
245
+ - Floats: Żmij shortest round-trip core (one 64x128 multiply), SSE BCD digit conversion and `pshufb`
246
+ decimal-point insertion (ported from zmij), exponent table, 4-float batches.
247
+ - Ints: itoap-style writer, and 4-at-a-time SIMD batches using zmij's 16-bit-lane digit trick.
248
+ - Strings: compact-ASCII / cached-UTF-8 access (as orjson), 64-byte SIMD escape scan with ctz jump,
249
+ page-safe 32-byte masked tail.
250
+ - Dicts: entries are read straight from CPython's key table (no `PyDict_Next`
251
+ call per key; split tables fall back to it), register-resident write cursor,
252
+ per-call key cache (repeated key objects copy their escaped bytes), and inline
253
+ `true`/`false`/`{}`/`[]` values. CPython's key-table kind keeps Unicode-only
254
+ dictionaries on this writer even with `OPT_NON_STR_KEYS`. `OPT_SORT_KEYS` and
255
+ non-str keys snapshot the entries as native records (stack storage up to 31
256
+ entries), sorted with an inlined insertion/quicksort rather than `qsort`
257
+ callbacks; int, float, bool and None keys are converted to text without
258
+ creating Python strings.
259
+ - Lists: 4-wide SIMD batches for ints and floats, and short leaf lists of
260
+ numbers (coordinate pairs) written in place without a nested call.
261
+ - NumPy: buffer-protocol fast path for contiguous float64/float32/int64/int32/uint8/bool, `.tolist()` fallback.
262
+
263
+ `tools/` holds the Python reference implementations used to validate the float core
264
+ (`zmij_reference.py`, `ref.py`) and zmij's power-of-ten table.
265
+
266
+ ## Limitations
267
+
268
+ - NaN/Infinity output intentionally differs from orjson's `null`. The strict
269
+ decoder rejects these tokens, so nonfinite output does not round-trip through `loads`.
270
+ - NumPy float32 values are formatted after promotion to float64, which can
271
+ produce more decimal digits than orjson. Non-contiguous arrays use `.tolist()`.
272
+ - CPython 3.11 - 3.15 default (GIL) builds only: free-threaded (`t`) builds lay
273
+ objects out differently and are rejected at build time. One build serves one
274
+ minor version. x86-64 AVX2 by default; `MCPU=x86-64-v4 ./build.sh` builds
275
+ an AVX-512 variant (k-mask string scanning) that only runs on such CPUs and
276
+ was slower on a Cascade Lake Xeon (512-bit frequency penalty), so there is
277
+ no runtime dispatch.
278
+ - Keep `build/_mojson_support.py` alongside the built `mojson.*.so`; the build copies
279
+ this stdlib-only helper automatically. The extension has no orjson runtime dependency.
280
+ - The string tail reads up to 31 bytes past a string's end within the same memory page (safe, but
281
+ AddressSanitizer/valgrind will flag it).
282
+ - A `.so` straight from `build.sh` needs the Mojo runtime libraries (`libKGENCompilerRTShared.so`,
283
+ `libAsyncRTRuntimeGlobals.so`, `libMSupportGlobals.so`) from the `mojo` pip package, found
284
+ through the RUNPATH the compiler records. The published wheels bundle them (`auditwheel repair`).
285
+ At import the module points the Mojo runtime at the running interpreter (it sets
286
+ `MOJO_PYTHON_LIBRARY` when unset), so no `python3` needs to be on `PATH`. If `MOJO_PYTHON`
287
+ or `MOJO_PYTHON_LIBRARY` is already set, the runtime uses it, so it must be valid.
288
+
289
+ ## Credits
290
+
291
+ Float algorithm and power-of-ten table from Żmij by Victor Zverovich (https://github.com/vitaut/zmij, MIT);
292
+ integer writer structure from itoap; design informed by orjson's source (https://github.com/ijl/orjson).
293
+ See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for the upstream notices.
yjson-0.1.0/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # mojson
2
+
3
+ A JSON serializer for CPython written in Mojo, with an orjson-style API.
4
+ NaN / Infinity are written the way Python's `json` writes them
5
+ (`NaN`, `Infinity`, `-Infinity`), and NumPy arrays and scalars are supported directly.
6
+
7
+ ## Install
8
+
9
+ The package is published on PyPI as `yjson` (the name `mojson` is too close to an
10
+ existing project); the module you import is still `mojson`. Wheels are built for
11
+ CPython 3.11 - 3.15 on x86-64 Linux
12
+ (`manylinux_2_35`, i.e. glibc 2.35+ such as Ubuntu 22.04 or newer) and need a CPU
13
+ with AVX2. They bundle the Mojo runtime library, so nothing else is required:
14
+
15
+ ```bash
16
+ pip install yjson # or: uv add yjson
17
+ python -c 'import mojson; print(mojson.dumps({"ok": True}))'
18
+ ```
19
+
20
+ Building from the sdist needs the Mojo compiler on `PATH` (see below).
21
+
22
+ ## Build
23
+
24
+ Requirements: CPython 3.11, 3.12, 3.13, 3.14 or 3.15 (default GIL builds) with headers,
25
+ a C compiler, x86-64 with AVX2, and Mojo 1.1 (`pip install mojo`). The build
26
+ targets one interpreter at a time: it uses `.venv-bench/bin/python` if present,
27
+ otherwise `python3`; override with `PYTHON=...`.
28
+
29
+ ```bash
30
+ ./build.sh # -> build/mojson.cpython-312-x86_64-linux-gnu.so
31
+ PYTHON=python3.14 ./build.sh # -> build/mojson.cpython-314-x86_64-linux-gnu.so
32
+ export PYTHONPATH="$PWD/build${PYTHONPATH:+:$PYTHONPATH}"
33
+ ```
34
+
35
+ The output carries the interpreter's extension suffix, so builds for several
36
+ versions coexist in `build/` and each interpreter imports its own. The Mojo
37
+ loops read CPython object fields directly; `build.sh` probes those offsets from
38
+ the target headers (`src/layout_probe.c`) and passes them to both compilers.
39
+ The C shim re-checks them with `_Static_assert`, and the module verifies them
40
+ against live objects at import, so an unsupported interpreter fails with an
41
+ `ImportError` instead of reading memory wrongly. The layout differences in this
42
+ range are small: 3.11 has a longer str header (the legacy `wstr` field) and
43
+ keeps an int's sign and digit count in `ob_size`, for which the build selects a
44
+ tag-synthesizing variant that leaves the 3.12+ code untouched; 3.14 moved tuple
45
+ items by adding a cached tuple hash.
46
+
47
+ ## Use
48
+
49
+ ```python
50
+ import mojson
51
+ mojson.dumps({"a": [1, 2.5, None], "b": float("nan")}) # b'{"a":[1,2.5,null],"b":NaN}'
52
+ mojson.dumps(obj).decode() # if you need a str
53
+ mojson.loads(b'{"a": 1}') # -> {"a": 1}
54
+ ```
55
+
56
+ See `examples/usage.py` (NumPy, NaN, files, errors).
57
+
58
+ `dumps(obj, /, default=None, option=None)` returns bytes. In addition to the
59
+ basic JSON types, it supports datetime/date/time, UUID, Enum, dataclass
60
+ instances, and subclasses of str/int/list/dict. Dataclass fields beginning
61
+ with `_` are omitted. Tuple subclasses use `default` rather than being treated
62
+ as arrays. Encoding errors are `JSONEncodeError`, an alias of `TypeError`;
63
+ exceptions raised by a default callback are attached as `__cause__`.
64
+
65
+ ```python
66
+ import datetime
67
+ import decimal
68
+ import mojson
69
+
70
+ data = {"created": datetime.datetime(2024, 1, 2), "amount": decimal.Decimal("12.50")}
71
+ out = mojson.dumps(
72
+ data,
73
+ default=str,
74
+ option=mojson.OPT_NAIVE_UTC | mojson.OPT_UTC_Z | mojson.OPT_INDENT_2,
75
+ )
76
+ mojson.dumps({2: "b", 1: "a"}, option=mojson.OPT_NON_STR_KEYS | mojson.OPT_SORT_KEYS)
77
+ mojson.dumps({"cached": mojson.Fragment(b'{"a":1}')})
78
+ ```
79
+
80
+ Options use the same bit values as [orjson](https://github.com/ijl/orjson#option):
81
+ `OPT_INDENT_2`, `OPT_SORT_KEYS`, `OPT_NON_STR_KEYS`, `OPT_STRICT_INTEGER`,
82
+ `OPT_APPEND_NEWLINE`, `OPT_NAIVE_UTC`, `OPT_UTC_Z`, `OPT_OMIT_MICROSECONDS`,
83
+ and `OPT_PASSTHROUGH_DATACLASS`, `OPT_PASSTHROUGH_DATETIME`,
84
+ `OPT_PASSTHROUGH_SUBCLASS`. `OPT_SERIALIZE_NUMPY` is accepted; NumPy support
85
+ is already automatic. The deprecated `OPT_SERIALIZE_DATACLASS` and
86
+ `OPT_SERIALIZE_UUID` are zero. Flags can be combined with `|`.
87
+
88
+ Integers support the range `-2**63` through `2**64 - 1`; strict mode restricts
89
+ values to `-(2**53 - 1)` through `2**53 - 1`. Non-string integer keys retain
90
+ the 64-bit range in strict mode. Non-string key conversion preserves duplicate
91
+ JSON keys. Fragments insert their contents verbatim, including under indentation;
92
+ validate the contents yourself when needed.
93
+
94
+ `OPT_INDENT_2`, `OPT_SORT_KEYS` and `OPT_NON_STR_KEYS` run on the same
95
+ compiled writers as the compact default (indentation is a compile-time variant
96
+ of those loops; sorting and non-str keys snapshot the dict as native records),
97
+ so they stay close to orjson's speed for the same option. `OPT_STRICT_INTEGER`
98
+ and `OPT_PASSTHROUGH_SUBCLASS` use a generic traversal that checks every value
99
+ and is slower. Custom conversions and uncommon types cost additional work;
100
+ measure their speed on your payload (`bench/bench_shapes.py`). `loads` uses Python's standard parser with UTF-8, nonfinite-number,
101
+ and surrogate checks. It accepts str/bytes/bytearray/contiguous memoryview and
102
+ raises `JSONDecodeError` (a subclass of `json.JSONDecodeError`). Its parsing
103
+ speed and maximum nesting follow the stdlib backend, rather than orjson's parser.
104
+
105
+ `dumps_socket(obj, /, default=None)` is a separate native encoder for Reflex
106
+ wire packets. It accepts arbitrary-size integers (subject to CPython's decimal
107
+ digit limit), preserves NaN/Infinity and `None`, escapes lone surrogates, and
108
+ protects strings that collide with Reflex's special-value markers. It returns
109
+ bytes and accepts no formatting options. Framework custom types use `default`.
110
+ The integration in `bench/reflex_codec.py` replaces the socket boundary with
111
+ this function, eliminating stdlib retries and repeated Python container walks.
112
+ Ordinary `dumps()` keeps its integer range and UTF-8 error behavior.
113
+
114
+ ## Test and benchmark
115
+
116
+ ```bash
117
+ pip install orjson numpy
118
+ python tests/check_correctness.py path/to/jsonexamples # corpus optional
119
+ python tests/check_features.py # options, types, callbacks, decoding
120
+ python bench/bench_paired.py path/to/jsonexamples # mojson vs orjson, robust
121
+ python bench/bench_features.py # enabled feature paths vs orjson
122
+ python bench/bench_shapes.py --cpu 2 # per payload shape vs orjson (where it wins or loses)
123
+ python bench/bench_regression.py # requires a baseline build in build/baseline/
124
+ python bench/bench_all_libraries.py path/to/jsonexamples # + msgspec, ujson, rapidjson, json, simplejson
125
+ python bench/bench_numpy.py
126
+ ```
127
+
128
+ Benchmark corpus: `jsonexamples/` from https://github.com/simdjson/simdjson-data.
129
+
130
+ The [Reflex PR 6116 comparison](bench/REFLEX.md) documents a pinned framework
131
+ checkout, unchanged upstream codec tests, paired encode/event benchmarks, and
132
+ real browser checks for both dump backends. Published measurements and validation
133
+ are in [bench/results](bench/results/README.md), including the general 14-file
134
+ comparison, native socket benchmarks and the default-path regression check.
135
+ The same directory holds the [CPython 3.11 - 3.15 measurements](bench/results/README.md#cpython-311---315-version-port)
136
+ and the [profile-driven improvements](bench/results/README.md#profile-driven-encoder-improvements-cpython-314)
137
+ measured on 3.14 (corpus 1.18× faster than before them; 1.28–1.32× of orjson on every interpreter).
138
+ The [all-events Reflex run](bench/results/README.md#reflex-pr-6116-every-event-workload)
139
+ measures every workload of the PR's event benchmark: all at parity, because
140
+ those deltas spend 88–97% of their encode time in Reflex's Python `default()`
141
+ serializer for pydantic models, which neither codec can skip.
142
+ The orjson baseline retains the PR's original
143
+ codec; mojson replaces its socket retry logic with native serialization.
144
+ Earlier option and indentation results remain in `build/fix-options.json`
145
+ and `build/indent-reflex-benchmark.json`.
146
+
147
+ ### Packaging and CI
148
+
149
+ `pyproject.toml` describes the package; `setup.py` only teaches setuptools to
150
+ compile the extension through `build.sh`, so `uv build` and `pip install .`
151
+ work with the Mojo compiler on `PATH`. The dependency groups `test`, `wheel`
152
+ and `mojo` are pinned in `uv.lock`:
153
+
154
+ ```bash
155
+ UV_PROJECT_ENVIRONMENT=.venv-mojo uv sync --only-group mojo --python 3.13 # Mojo 1.1
156
+ export PATH="$PWD/.venv-mojo/bin:$PATH"
157
+ uv build --sdist
158
+ uv build --wheel --python 3.13 --out-dir dist dist/yjson-*.tar.gz # cp313 wheel
159
+ uv sync --only-group wheel # auditwheel + patchelf into .venv
160
+ PATH="$PWD/.venv/bin:$PATH" auditwheel repair \
161
+ --ldpaths "$(.venv-mojo/bin/python -c 'import modular; print(modular.__path__[0] + "/lib")')" \
162
+ --disable-isa-ext-check --wheel-dir wheelhouse dist/*.whl
163
+ ```
164
+
165
+ `auditwheel repair` copies `libKGENCompilerRTShared.so` and its dependencies
166
+ into `mojson.libs/` and sets the `manylinux_2_35` tag (the floor set by the Mojo
167
+ runtime). `--disable-isa-ext-check` is required because the extension targets
168
+ x86-64-v3 (AVX2) by design.
169
+
170
+ [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs the same steps on
171
+ every push and pull request: it builds the sdist, builds one wheel per
172
+ interpreter from that sdist, repairs it, installs it into a clean environment
173
+ and runs `tests/check_features.py`, `tests/check_correctness.py` (with the
174
+ simdjson corpus) and `examples/usage.py` against the installed wheel. Pushing a
175
+ tag `vX.Y.Z` whose version matches `pyproject.toml` additionally publishes the
176
+ sdist and wheels to PyPI through [trusted publishing](https://docs.pypi.org/trusted-publishers/)
177
+ from the `pypi` GitHub environment, so no API token is stored. To release:
178
+ bump `version` in `pyproject.toml`, commit, then `git tag v0.1.0 && git push origin v0.1.0`.
179
+
180
+ ### Local comparison with uv
181
+
182
+ If Mojo is already installed in `.venv`, keep that compiler environment and use
183
+ a separate CPython environment for the extension (3.12 shown; any of 3.11 - 3.15 works):
184
+
185
+ ```bash
186
+ uv venv --python 3.12 .venv-bench
187
+ uv pip install --python .venv-bench/bin/python orjson numpy
188
+ PATH="$PWD/.venv/bin:$PATH" ./build.sh
189
+ .venv-bench/bin/python tools/fetch_corpus.py
190
+ .venv-bench/bin/python tests/check_correctness.py build/jsonexamples
191
+ .venv-bench/bin/python tests/check_features.py
192
+ .venv-bench/bin/python bench/bench_paired.py build/jsonexamples --cpu 2 --micro --output build/paired-local.json
193
+ ```
194
+
195
+ To build and test every supported version side by side:
196
+
197
+ ```bash
198
+ for v in 3.11 3.13 3.14 3.15; do
199
+ uv venv --python $v .venv-py$v && uv pip install --python .venv-py$v/bin/python orjson numpy
200
+ PATH="$PWD/.venv/bin:$PATH" PYTHON=.venv-py$v/bin/python ./build.sh
201
+ .venv-py$v/bin/python tests/check_correctness.py build/jsonexamples
202
+ .venv-py$v/bin/python tests/check_features.py
203
+ done
204
+ ```
205
+
206
+ Choose an available logical CPU for `--cpu`, or omit it. The paired benchmark
207
+ checks output equality, warms both serializers, calibrates a common batch size
208
+ to at least 10 ms, and alternates order across 40 pairs. It reports median time
209
+ per call, the median `orjson time / mojson time` ratio, and ratio quartiles.
210
+ Ratios above 1 mean mojson is faster. The optional JSON report includes every
211
+ sample and environment metadata. The corpus downloader records the source
212
+ revision and SHA-256 hashes in `build/jsonexamples/manifest.json.txt`.
213
+
214
+ ## What's inside (src/mojson.mojo)
215
+
216
+ - Direct reads of CPython object layouts (type pointer, list/tuple items, compact ints, float bits, str data)
217
+ for 3.11 - 3.15, with the offsets supplied by the build and verified at import,
218
+ and `external_call` into the CPython C API; output written straight into a `bytes` object.
219
+ - Floats: Żmij shortest round-trip core (one 64x128 multiply), SSE BCD digit conversion and `pshufb`
220
+ decimal-point insertion (ported from zmij), exponent table, 4-float batches.
221
+ - Ints: itoap-style writer, and 4-at-a-time SIMD batches using zmij's 16-bit-lane digit trick.
222
+ - Strings: compact-ASCII / cached-UTF-8 access (as orjson), 64-byte SIMD escape scan with ctz jump,
223
+ page-safe 32-byte masked tail.
224
+ - Dicts: entries are read straight from CPython's key table (no `PyDict_Next`
225
+ call per key; split tables fall back to it), register-resident write cursor,
226
+ per-call key cache (repeated key objects copy their escaped bytes), and inline
227
+ `true`/`false`/`{}`/`[]` values. CPython's key-table kind keeps Unicode-only
228
+ dictionaries on this writer even with `OPT_NON_STR_KEYS`. `OPT_SORT_KEYS` and
229
+ non-str keys snapshot the entries as native records (stack storage up to 31
230
+ entries), sorted with an inlined insertion/quicksort rather than `qsort`
231
+ callbacks; int, float, bool and None keys are converted to text without
232
+ creating Python strings.
233
+ - Lists: 4-wide SIMD batches for ints and floats, and short leaf lists of
234
+ numbers (coordinate pairs) written in place without a nested call.
235
+ - NumPy: buffer-protocol fast path for contiguous float64/float32/int64/int32/uint8/bool, `.tolist()` fallback.
236
+
237
+ `tools/` holds the Python reference implementations used to validate the float core
238
+ (`zmij_reference.py`, `ref.py`) and zmij's power-of-ten table.
239
+
240
+ ## Limitations
241
+
242
+ - NaN/Infinity output intentionally differs from orjson's `null`. The strict
243
+ decoder rejects these tokens, so nonfinite output does not round-trip through `loads`.
244
+ - NumPy float32 values are formatted after promotion to float64, which can
245
+ produce more decimal digits than orjson. Non-contiguous arrays use `.tolist()`.
246
+ - CPython 3.11 - 3.15 default (GIL) builds only: free-threaded (`t`) builds lay
247
+ objects out differently and are rejected at build time. One build serves one
248
+ minor version. x86-64 AVX2 by default; `MCPU=x86-64-v4 ./build.sh` builds
249
+ an AVX-512 variant (k-mask string scanning) that only runs on such CPUs and
250
+ was slower on a Cascade Lake Xeon (512-bit frequency penalty), so there is
251
+ no runtime dispatch.
252
+ - Keep `build/_mojson_support.py` alongside the built `mojson.*.so`; the build copies
253
+ this stdlib-only helper automatically. The extension has no orjson runtime dependency.
254
+ - The string tail reads up to 31 bytes past a string's end within the same memory page (safe, but
255
+ AddressSanitizer/valgrind will flag it).
256
+ - A `.so` straight from `build.sh` needs the Mojo runtime libraries (`libKGENCompilerRTShared.so`,
257
+ `libAsyncRTRuntimeGlobals.so`, `libMSupportGlobals.so`) from the `mojo` pip package, found
258
+ through the RUNPATH the compiler records. The published wheels bundle them (`auditwheel repair`).
259
+ At import the module points the Mojo runtime at the running interpreter (it sets
260
+ `MOJO_PYTHON_LIBRARY` when unset), so no `python3` needs to be on `PATH`. If `MOJO_PYTHON`
261
+ or `MOJO_PYTHON_LIBRARY` is already set, the runtime uses it, so it must be valid.
262
+
263
+ ## Credits
264
+
265
+ Float algorithm and power-of-ten table from Żmij by Victor Zverovich (https://github.com/vitaut/zmij, MIT);
266
+ integer writer structure from itoap; design informed by orjson's source (https://github.com/ijl/orjson).
267
+ See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for the upstream notices.
@@ -0,0 +1,63 @@
1
+ # Third-party notices
2
+
3
+ These notices apply to the derived portions identified below.
4
+
5
+ ## Żmij
6
+
7
+ The floating-point core, digit conversion and power-of-ten table in
8
+ `src/mojson.mojo` and `tools/` are ported from
9
+ [Żmij](https://github.com/vitaut/zmij).
10
+ [Upstream license](https://github.com/vitaut/zmij/blob/main/LICENSE):
11
+
12
+ ```text
13
+ MIT License
14
+
15
+ Copyright (c) 2025 Victor Zverovich
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights
20
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ copies of the Software, and to permit persons to whom the Software is
22
+ furnished to do so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in all
25
+ copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
34
+ ```
35
+
36
+ ## itoap
37
+
38
+ The integer writer in `src/mojson.mojo` is ported from
39
+ [itoap](https://github.com/Kogia-sima/itoap), version 1.0.1.
40
+ The notice below is preserved from that crate.
41
+
42
+ ```text
43
+ The MIT License (MIT)
44
+ Copyright (c) 2014-2016 Milo Yip, 2020 Ryohei Machida
45
+
46
+ Permission is hereby granted, free of charge, to any person obtaining a copy
47
+ of this software and associated documentation files (the "Software"), to deal
48
+ in the Software without restriction, including without limitation the rights
49
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
50
+ copies of the Software, and to permit persons to whom the Software is
51
+ furnished to do so, subject to the following conditions:
52
+
53
+ The above copyright notice and this permission notice shall be included in all
54
+ copies or substantial portions of the Software.
55
+
56
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
57
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
58
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
59
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
60
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
61
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
62
+ OR OTHER DEALINGS IN THE SOFTWARE.
63
+ ```