File Coverage

YY.xs
Criterion Covered Total %
statement 1134 1289 87.9
branch 541 874 61.9
condition n/a
subroutine n/a
pod n/a
total 1675 2163 77.4


line stmt bran cond sub pod time code
1             #define PERL_NO_GET_CONTEXT
2             #include "EXTERN.h"
3             #include "perl.h"
4             #include "XSUB.h"
5              
6             #include "yyjson.h"
7             #include "XSParseKeyword.h"
8              
9             #include
10              
11             /* flags stored in the JSON::YY object (IV inside the hash) */
12             #define F_UTF8 0x01
13             #define F_PRETTY 0x02
14             #define F_CANONICAL 0x04
15             #define F_ALLOW_NONREF 0x08
16             #define F_ALLOW_UNKNOWN 0x10
17             #define F_ALLOW_BLESSED 0x20
18             #define F_CONVERT_BLESSED 0x40
19              
20             #define MAX_DEPTH_DEFAULT 512
21              
22             typedef struct {
23             U32 flags;
24             U32 max_depth;
25             } json_yy_t;
26              
27             /* magic vtable for json_yy_t stored on HV */
28             static int
29 16           json_yy_magic_free(pTHX_ SV *sv, MAGIC *mg) {
30             PERL_UNUSED_ARG(sv);
31 16 50         if (mg->mg_ptr)
32 16           Safefree(mg->mg_ptr);
33 16           return 0;
34             }
35              
36             static MGVTBL json_yy_vtbl = {
37             NULL, NULL, NULL, NULL,
38             json_yy_magic_free,
39             NULL, NULL, NULL
40             };
41              
42             static inline json_yy_t *
43 53           get_self(pTHX_ SV *self_sv) {
44 53 50         if (!SvROK(self_sv))
45 0           croak("not a JSON::YY object");
46 53           MAGIC *mg = mg_findext(SvRV(self_sv), PERL_MAGIC_ext, &json_yy_vtbl);
47 53 50         if (!mg)
48 0           croak("corrupted JSON::YY object");
49 53           return (json_yy_t *)mg->mg_ptr;
50             }
51              
52             static MGVTBL empty_vtbl = {0};
53              
54             /* forward declarations */
55             static inline int is_ascii(const char *s, size_t len);
56             /* doc holder magic: frees yyjson_doc when SV is destroyed */
57             static int
58 10           docholder_magic_free(pTHX_ SV *sv, MAGIC *mg) {
59             PERL_UNUSED_ARG(sv);
60 10           yyjson_doc *doc = (yyjson_doc *)mg->mg_ptr;
61 10 50         if (doc)
62             yyjson_doc_free(doc);
63 10           return 0;
64             }
65              
66             /* also used as a guard for yyjson_mut_doc* via mg_ptr cast */
67             static int
68 5012           mut_docholder_magic_free(pTHX_ SV *sv, MAGIC *mg) {
69             PERL_UNUSED_ARG(sv);
70 5012           yyjson_mut_doc *doc = (yyjson_mut_doc *)mg->mg_ptr;
71 5012 100         if (doc)
72 2           yyjson_mut_doc_free(doc);
73 5012           return 0;
74             }
75              
76             static MGVTBL docholder_magic_vtbl = {
77             NULL, NULL, NULL, NULL,
78             docholder_magic_free,
79             NULL, NULL, NULL
80             };
81              
82             static MGVTBL mut_docholder_vtbl = {
83             NULL, NULL, NULL, NULL,
84             mut_docholder_magic_free,
85             NULL, NULL, NULL
86             };
87             static SV * yyjson_val_to_sv(pTHX_ yyjson_val *val);
88             static SV * yyjson_val_to_sv_ro(pTHX_ yyjson_val *val, SV *doc_sv);
89             static yyjson_mut_val * sv_to_yyjson_val(pTHX_ yyjson_mut_doc *doc, SV *sv,
90             json_yy_t *self, U32 depth);
91              
92             /* ---- JSON::YY::Doc -- opaque yyjson mutable document handle ---- */
93              
94             typedef struct {
95             yyjson_mut_doc *doc; /* the mutable document */
96             yyjson_mut_val *root; /* value this handle points at (may be subtree) */
97             SV *owner; /* NULL=owns doc, non-NULL=RV to parent Doc (borrowed) */
98             } json_yy_doc_t;
99              
100             static int
101 21135           json_yy_doc_magic_free(pTHX_ SV *sv, MAGIC *mg) {
102             PERL_UNUSED_ARG(sv);
103 21135           json_yy_doc_t *d = (json_yy_doc_t *)mg->mg_ptr;
104 21135 50         if (d) {
105 21135 100         if (d->owner) {
106 1033           SvREFCNT_dec(d->owner);
107             } else {
108 20102 50         if (d->doc)
109 20102           yyjson_mut_doc_free(d->doc);
110             }
111 21135           Safefree(d);
112             }
113 21135           return 0;
114             }
115              
116             static MGVTBL json_yy_doc_vtbl = {
117             NULL, NULL, NULL, NULL,
118             json_yy_doc_magic_free,
119             NULL, NULL, NULL
120             };
121              
122             static inline json_yy_doc_t *
123 30251           get_doc(pTHX_ SV *sv) {
124 30251 50         if (!SvROK(sv))
125 0           croak("not a JSON::YY::Doc object");
126 30251           MAGIC *mg = mg_findext(SvRV(sv), PERL_MAGIC_ext, &json_yy_doc_vtbl);
127 30251 50         if (!mg)
128 0           croak("corrupted JSON::YY::Doc object");
129 30251           return (json_yy_doc_t *)mg->mg_ptr;
130             }
131              
132             /* create a new Doc SV. if owner is non-NULL, this is a borrowing ref. */
133             static SV *
134 21135           new_doc_sv(pTHX_ yyjson_mut_doc *doc, yyjson_mut_val *root, SV *owner) {
135             json_yy_doc_t *d;
136 21135           HV *hv = newHV();
137 21135           Newxz(d, 1, json_yy_doc_t);
138 21135           d->doc = doc;
139 21135           d->root = root;
140 21135 100         if (owner) {
141 1033           d->owner = owner;
142 1033           SvREFCNT_inc_simple_void_NN(owner);
143             }
144 21135           sv_magicext((SV *)hv, NULL, PERL_MAGIC_ext, &json_yy_doc_vtbl,
145             (const char *)d, 0);
146 21135           return sv_bless(newRV_noinc((SV *)hv),
147             gv_stashpvs("JSON::YY::Doc", GV_ADD));
148             }
149              
150             /* resolve a path on a Doc, returning the yyjson_mut_val* or NULL.
151             path must be UTF-8 encoded (use SvPVutf8 on caller side). */
152             static inline yyjson_mut_val *
153 20180           doc_resolve_path(pTHX_ json_yy_doc_t *d, const char *path, STRLEN path_len) {
154 20180 100         if (path_len == 0)
155 15076           return d->root;
156 10208 50         return yyjson_mut_ptr_getn(d->root, path, path_len);
157             }
158              
159              
160             /* forward decl */
161             static SV * yyjson_mut_val_to_sv(pTHX_ yyjson_mut_val *val);
162              
163             /* ---- keyword plugin: Doc keyword ops ---- */
164              
165             /* pp_jdoc: parse JSON string → Doc */
166 10071           static OP * pp_jdoc_impl(pTHX) {
167 10071           dSP;
168 10071           SV *json_sv = POPs;
169             STRLEN len;
170 10071           const char *json = SvPVutf8(json_sv, len);
171              
172             yyjson_read_err err;
173 10071           yyjson_doc *idoc = yyjson_read_opts((char *)json, len, YYJSON_READ_NOFLAG, NULL, &err);
174 10071 100         if (!idoc)
175 1           croak("jdoc: JSON parse error: %s at byte offset %zu", err.msg, err.pos);
176              
177 10070           yyjson_mut_doc *mdoc = yyjson_doc_mut_copy(idoc, NULL);
178             yyjson_doc_free(idoc);
179 10070 50         if (!mdoc)
180 0           croak("jdoc: failed to create mutable document");
181              
182 10070           yyjson_mut_val *root = yyjson_mut_doc_get_root(mdoc);
183 10070           SV *result = new_doc_sv(aTHX_ mdoc, root, NULL);
184 10070 50         XPUSHs(sv_2mortal(result));
185 10070           RETURN;
186             }
187              
188             /* pp_jget: get subtree ref (borrowing) */
189 8           static OP * pp_jget_impl(pTHX) {
190 8           dSP;
191 8           SV *path_sv = POPs;
192 8           SV *doc_sv = POPs;
193 8           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
194             STRLEN path_len;
195 8           const char *path = SvPVutf8(path_sv, path_len);
196              
197 8           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
198 8 100         if (!val)
199 1           croak("jget: path not found: %.*s", (int)path_len, path);
200              
201 7           SV *result = new_doc_sv(aTHX_ d->doc, val, doc_sv);
202 7 50         XPUSHs(sv_2mortal(result));
203 7           RETURN;
204             }
205              
206             /* pp_jgetp: get as Perl value */
207 54           static OP * pp_jgetp_impl(pTHX) {
208 54           dSP;
209 54           SV *path_sv = POPs;
210 54           SV *doc_sv = POPs;
211 54           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
212             STRLEN path_len;
213 54           const char *path = SvPVutf8(path_sv, path_len);
214              
215 54           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
216 54 100         if (!val) {
217 1 50         XPUSHs(&PL_sv_undef);
218 1           RETURN;
219             }
220              
221 53           SV *result = yyjson_mut_val_to_sv(aTHX_ val);
222 53 50         XPUSHs(sv_2mortal(result));
223 53           RETURN;
224             }
225              
226             /* pp_jset: set value at path */
227 10023           static OP * pp_jset_impl(pTHX) {
228 10023           dSP;
229 10023           SV *value_sv = POPs;
230 10023           SV *path_sv = POPs;
231 10023           SV *doc_sv = POPs;
232 10023           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
233             STRLEN path_len;
234 10023           const char *path = SvPVutf8(path_sv, path_len);
235              
236             yyjson_mut_val *new_val;
237              
238             /* check if value is a Doc */
239 10033 100         if (SvROK(value_sv) && sv_derived_from(value_sv, "JSON::YY::Doc")) {
    100          
240 10           json_yy_doc_t *vd = get_doc(aTHX_ value_sv);
241 10           new_val = yyjson_mut_val_mut_copy(d->doc, vd->root);
242 10 50         if (!new_val)
243 0           croak("jset: failed to copy Doc value");
244             } else {
245             /* convert Perl value to yyjson_mut_val */
246             json_yy_t self_stack;
247 10013           self_stack.flags = F_UTF8 | F_ALLOW_NONREF | F_ALLOW_BLESSED;
248 10013           self_stack.max_depth = MAX_DEPTH_DEFAULT;
249 10013           new_val = sv_to_yyjson_val(aTHX_ d->doc, value_sv, &self_stack, 0);
250             }
251              
252 10023 100         if (path_len == 0) {
253 2 100         if (d->owner)
254 1           croak("jset: cannot replace root of a borrowed Doc; jclone it first");
255 1 50         yyjson_mut_doc_set_root(d->doc, new_val);
256 1           d->root = new_val;
257             } else {
258             yyjson_ptr_err perr;
259             /* try set first; if path ends with /- (array append), use add instead */
260 10021 50         bool ok = yyjson_mut_doc_ptr_setx(d->doc, path, path_len, new_val,
261             true, NULL, &perr);
262 10021 100         if (!ok) {
263             /* retry with add (handles /- array append) */
264 4           perr = (yyjson_ptr_err){0};
265 8 50         ok = yyjson_mut_doc_ptr_addx(d->doc, path, path_len, new_val,
266             true, NULL, &perr);
267             }
268 10021 50         if (!ok)
269 0 0         croak("jset: failed to set path %.*s: %s",
270             (int)path_len, path, perr.msg ? perr.msg : "unknown error");
271             }
272              
273 10022 50         XPUSHs(doc_sv);
274 10022           RETURN;
275             }
276              
277             /* pp_jdel: delete at path, return removed subtree as Doc */
278 5           static OP * pp_jdel_impl(pTHX) {
279 5           dSP;
280 5           SV *path_sv = POPs;
281 5           SV *doc_sv = POPs;
282 5           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
283             STRLEN path_len;
284 5           const char *path = SvPVutf8(path_sv, path_len);
285              
286 5 100         if (path_len == 0)
287 1           croak("jdel: cannot delete root");
288              
289 4           yyjson_ptr_ctx ctx = {0};
290             yyjson_ptr_err perr;
291 4 50         yyjson_mut_val *removed = yyjson_mut_doc_ptr_removex(d->doc, path, path_len,
292             &ctx, &perr);
293 4 100         if (!removed) {
294 2 50         XPUSHs(&PL_sv_undef);
295 2           RETURN;
296             }
297              
298             /* deep copy removed val into independent doc (safe from parent mutations) */
299 2           yyjson_mut_doc *new_doc = yyjson_mut_doc_new(NULL);
300 2           yyjson_mut_val *copy = yyjson_mut_val_mut_copy(new_doc, removed);
301             yyjson_mut_doc_set_root(new_doc, copy);
302 2           SV *result = new_doc_sv(aTHX_ new_doc, copy, NULL);
303 2 50         XPUSHs(sv_2mortal(result));
304 2           RETURN;
305             }
306              
307             /* pp_jhas: check if path exists */
308 8           static OP * pp_jhas_impl(pTHX) {
309 8           dSP;
310 8           SV *path_sv = POPs;
311 8           SV *doc_sv = POPs;
312 8           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
313             STRLEN path_len;
314 8           const char *path = SvPVutf8(path_sv, path_len);
315              
316 8           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
317 8 50         XPUSHs(val ? &PL_sv_yes : &PL_sv_no);
    100          
318 8           RETURN;
319             }
320              
321             /* pp_jclone: deep copy doc/subtree → new independent Doc */
322 5003           static OP * pp_jclone_impl(pTHX) {
323 5003           dSP;
324 5003           SV *path_sv = POPs;
325 5003           SV *doc_sv = POPs;
326 5003           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
327             STRLEN path_len;
328 5003           const char *path = SvPVutf8(path_sv, path_len);
329              
330 5003           yyjson_mut_val *src = doc_resolve_path(aTHX_ d, path, path_len);
331 5003 50         if (!src)
332 0           croak("jclone: path not found: %.*s", (int)path_len, path);
333              
334 5003           yyjson_mut_doc *new_doc = yyjson_mut_doc_new(NULL);
335 5003 50         if (!new_doc)
336 0           croak("jclone: failed to create document");
337              
338 5003           yyjson_mut_val *new_root = yyjson_mut_val_mut_copy(new_doc, src);
339 5003 50         if (!new_root) {
340 0           yyjson_mut_doc_free(new_doc);
341 0           croak("jclone: failed to copy value");
342             }
343             yyjson_mut_doc_set_root(new_doc, new_root);
344              
345 5003           SV *result = new_doc_sv(aTHX_ new_doc, new_root, NULL);
346 5003 50         XPUSHs(sv_2mortal(result));
347 5003           RETURN;
348             }
349              
350             /* pp_jencode: serialize doc/subtree to JSON bytes */
351 15035           static OP * pp_jencode_impl(pTHX) {
352 15035           dSP;
353 15035           SV *path_sv = POPs;
354 15035           SV *doc_sv = POPs;
355 15035           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
356             STRLEN path_len;
357 15035           const char *path = SvPVutf8(path_sv, path_len);
358              
359 15035           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
360 15035 50         if (!val)
361 0           croak("jencode: path not found: %.*s", (int)path_len, path);
362              
363             size_t json_len;
364             yyjson_write_err werr;
365             char *json;
366              
367 15035 100         if (val == d->root && !d->owner) {
    100          
368             /* full doc -- use doc write */
369 15021           json = yyjson_mut_write_opts(d->doc, YYJSON_WRITE_NOFLAG, NULL, &json_len, &werr);
370             } else {
371             /* subtree -- use val write */
372 14           json = yyjson_mut_val_write_opts(val, YYJSON_WRITE_NOFLAG, NULL, &json_len, &werr);
373             }
374 15035 50         if (!json)
375 0           croak("jencode: write error: %s", werr.msg);
376              
377 15035           SV *result = newSVpvn(json, json_len);
378 15035           free(json);
379 15035 50         XPUSHs(sv_2mortal(result));
380 15035           RETURN;
381             }
382              
383             /* pp_jstr: create JSON string value */
384 4           static OP * pp_jstr_impl(pTHX) {
385 4           dSP;
386 4           SV *val_sv = POPs;
387             STRLEN len;
388 4           const char *str = SvPVutf8(val_sv, len);
389 4           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
390 8 50         yyjson_mut_val *root = yyjson_mut_strncpy(doc, str, len);
    50          
391             yyjson_mut_doc_set_root(doc, root);
392 4 50         XPUSHs(sv_2mortal(new_doc_sv(aTHX_ doc, root, NULL)));
393 4           RETURN;
394             }
395              
396             /* pp_jnum: create JSON number value */
397 3           static OP * pp_jnum_impl(pTHX) {
398 3           dSP;
399 3           SV *val_sv = POPs;
400 3           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
401             yyjson_mut_val *root;
402 3 100         if (SvIOK(val_sv)) {
403 2 50         if (SvIsUV(val_sv))
404 0 0         root = yyjson_mut_uint(doc, (uint64_t)SvUVX(val_sv));
405             else
406 4 50         root = yyjson_mut_sint(doc, (int64_t)SvIVX(val_sv));
407             } else {
408 2           root = yyjson_mut_real(doc, SvNV(val_sv));
409             }
410             yyjson_mut_doc_set_root(doc, root);
411 3 50         XPUSHs(sv_2mortal(new_doc_sv(aTHX_ doc, root, NULL)));
412 3           RETURN;
413             }
414              
415             /* pp_jbool: create JSON boolean */
416 4           static OP * pp_jbool_impl(pTHX) {
417 4           dSP;
418 4           SV *val_sv = POPs;
419 4           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
420 8 50         yyjson_mut_val *root = yyjson_mut_bool(doc, SvTRUE(val_sv));
    50          
421             yyjson_mut_doc_set_root(doc, root);
422 4 50         XPUSHs(sv_2mortal(new_doc_sv(aTHX_ doc, root, NULL)));
423 4           RETURN;
424             }
425              
426             /* pp_jnull: create JSON null */
427 2           static OP * pp_jnull_impl(pTHX) {
428 2           dSP;
429 2           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
430 2 50         yyjson_mut_val *root = yyjson_mut_null(doc);
431             yyjson_mut_doc_set_root(doc, root);
432 2 50         XPUSHs(sv_2mortal(new_doc_sv(aTHX_ doc, root, NULL)));
433 2           RETURN;
434             }
435              
436             /* pp_jarr: create empty JSON array */
437 3           static OP * pp_jarr_impl(pTHX) {
438 3           dSP;
439 3           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
440 3 50         yyjson_mut_val *root = yyjson_mut_arr(doc);
441             yyjson_mut_doc_set_root(doc, root);
442 3 50         XPUSHs(sv_2mortal(new_doc_sv(aTHX_ doc, root, NULL)));
443 3           RETURN;
444             }
445              
446             /* pp_jobj: create empty JSON object */
447 2           static OP * pp_jobj_impl(pTHX) {
448 2           dSP;
449 2           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
450 2 50         yyjson_mut_val *root = yyjson_mut_obj(doc);
451             yyjson_mut_doc_set_root(doc, root);
452 2 50         XPUSHs(sv_2mortal(new_doc_sv(aTHX_ doc, root, NULL)));
453 2           RETURN;
454             }
455              
456             /* pp_jtype: get type string */
457 8           static OP * pp_jtype_impl(pTHX) {
458 8           dSP;
459 8           SV *path_sv = POPs;
460 8           SV *doc_sv = POPs;
461 8           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
462             STRLEN path_len;
463 8           const char *path = SvPVutf8(path_sv, path_len);
464              
465 8           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
466 8 50         if (!val) {
467 0 0         XPUSHs(&PL_sv_undef);
468 0           RETURN;
469             }
470              
471             const char *type;
472 8           switch (yyjson_mut_get_type(val)) {
473 2           case YYJSON_TYPE_OBJ: type = "object"; break;
474 4           case YYJSON_TYPE_ARR: type = "array"; break;
475 1           case YYJSON_TYPE_STR: type = "string"; break;
476 1           case YYJSON_TYPE_NUM: type = "number"; break;
477 0           case YYJSON_TYPE_BOOL: type = "boolean"; break;
478 0           case YYJSON_TYPE_NULL: type = "null"; break;
479 0           default: type = "unknown"; break;
480             }
481 8 50         XPUSHs(sv_2mortal(newSVpv(type, 0)));
482 8           RETURN;
483             }
484              
485             /* pp_jlen: get array length or object key count */
486 9           static OP * pp_jlen_impl(pTHX) {
487 9           dSP;
488 9           SV *path_sv = POPs;
489 9           SV *doc_sv = POPs;
490 9           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
491             STRLEN path_len;
492 9           const char *path = SvPVutf8(path_sv, path_len);
493              
494 9           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
495 9 100         if (!val)
496 1           croak("jlen: path not found: %.*s", (int)path_len, path);
497              
498             size_t len;
499 8 100         if (yyjson_mut_is_arr(val))
500 5           len = yyjson_mut_arr_size(val);
501 3 100         else if (yyjson_mut_is_obj(val))
502 2           len = yyjson_mut_obj_size(val);
503 1 50         else if (yyjson_mut_is_str(val))
504 1           len = yyjson_mut_get_len(val);
505             else
506 0           croak("jlen: value at path is not a container or string");
507              
508 8 50         XPUSHs(sv_2mortal(newSViv((IV)len)));
509 8           RETURN;
510             }
511              
512             /* pp_jkeys: get object keys as list */
513 4           static OP * pp_jkeys_impl(pTHX) {
514 4           dSP;
515 4           SV *path_sv = POPs;
516 4           SV *doc_sv = POPs;
517 4           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
518             STRLEN path_len;
519 4           const char *path = SvPVutf8(path_sv, path_len);
520              
521 4           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
522 8 50         if (!val || !yyjson_mut_is_obj(val))
    100          
523 1           croak("jkeys: path does not point to an object");
524              
525             size_t idx, max;
526             yyjson_mut_val *key, *v;
527 6 50         EXTEND(SP, (SSize_t)yyjson_mut_obj_size(val));
    50          
    50          
528 11 50         yyjson_mut_obj_foreach(val, idx, max, key, v) {
    50          
    50          
    100          
529 5 50         const char *kstr = yyjson_mut_get_str(key);
530 5           size_t klen = yyjson_mut_get_len(key);
531 5           SV *ksv = newSVpvn(kstr, klen);
532 5 100         if (!is_ascii(kstr, klen))
533 1           SvUTF8_on(ksv);
534 5           PUSHs(sv_2mortal(ksv));
535             }
536 3           RETURN;
537             }
538              
539             /* ---- Iterator: pull-style for arrays and objects ---- */
540              
541             typedef struct {
542             union {
543             yyjson_mut_arr_iter arr;
544             yyjson_mut_obj_iter obj;
545             } it;
546             int is_obj;
547             yyjson_mut_val *cur_key; /* for objects: key from last jnext */
548             yyjson_mut_doc *doc;
549             SV *owner; /* refcounted parent Doc SV */
550             } json_yy_iter_t;
551              
552             static int
553 11           json_yy_iter_magic_free(pTHX_ SV *sv, MAGIC *mg) {
554             PERL_UNUSED_ARG(sv);
555 11           json_yy_iter_t *it = (json_yy_iter_t *)mg->mg_ptr;
556 11 50         if (it) {
557 11 50         if (it->owner)
558 11           SvREFCNT_dec(it->owner);
559 11           Safefree(it);
560             }
561 11           return 0;
562             }
563              
564             static MGVTBL json_yy_iter_vtbl = {
565             NULL, NULL, NULL, NULL,
566             json_yy_iter_magic_free,
567             NULL, NULL, NULL
568             };
569              
570             static inline json_yy_iter_t *
571 1029           get_iter(pTHX_ SV *sv) {
572 1029 50         if (!SvROK(sv))
573 0           croak("not a JSON::YY::Iter object");
574 1029           MAGIC *mg = mg_findext(SvRV(sv), PERL_MAGIC_ext, &json_yy_iter_vtbl);
575 1029 50         if (!mg)
576 0           croak("corrupted JSON::YY::Iter object");
577 1029           return (json_yy_iter_t *)mg->mg_ptr;
578             }
579              
580             /* pp_jiter: create iterator for array/object at path */
581 12           static OP * pp_jiter_impl(pTHX) {
582 12           dSP;
583 12           SV *path_sv = POPs;
584 12           SV *doc_sv = POPs;
585 12           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
586             STRLEN path_len;
587 12           const char *path = SvPVutf8(path_sv, path_len);
588              
589 12           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
590 12 50         if (!val)
591 0           croak("jiter: path not found: %.*s", (int)path_len, path);
592 15 100         if (!yyjson_mut_is_arr(val) && !yyjson_mut_is_obj(val))
    100          
593 1           croak("jiter: value at path is not an array or object");
594              
595             json_yy_iter_t *it;
596 11           Newxz(it, 1, json_yy_iter_t);
597 11           it->doc = d->doc;
598 11           it->owner = doc_sv;
599 11           SvREFCNT_inc_simple_void_NN(doc_sv);
600 11 50         it->cur_key = NULL;
601              
602 11 100         if (yyjson_mut_is_obj(val)) {
603 2           it->is_obj = 1;
604 2 50         yyjson_mut_obj_iter_init(val, &it->it.obj);
605             } else {
606 9           it->is_obj = 0;
607 9 50         yyjson_mut_arr_iter_init(val, &it->it.arr);
608             }
609              
610 11           HV *hv = newHV();
611 11           sv_magicext((SV *)hv, NULL, PERL_MAGIC_ext, &json_yy_iter_vtbl,
612             (const char *)it, 0);
613 11           SV *result = sv_bless(newRV_noinc((SV *)hv),
614             gv_stashpvs("JSON::YY::Iter", GV_ADD));
615 11 50         XPUSHs(sv_2mortal(result));
616 11           RETURN;
617             }
618              
619             /* pp_jnext: advance iterator, return next value as Doc or undef */
620 1026           static OP * pp_jnext_impl(pTHX) {
621 1026           dSP;
622 1026           SV *iter_sv = POPs;
623 1026           json_yy_iter_t *it = get_iter(aTHX_ iter_sv);
624              
625 1026           yyjson_mut_val *val = NULL;
626              
627 1026 100         if (it->is_obj) {
628 10 50         if (yyjson_mut_obj_iter_has_next(&it->it.obj)) {
    100          
629 3 50         it->cur_key = yyjson_mut_obj_iter_next(&it->it.obj);
630 6 50         val = yyjson_mut_obj_iter_get_val(it->cur_key);
631             }
632             } else {
633 2042 50         if (yyjson_mut_arr_iter_has_next(&it->it.arr)) {
    100          
634 2028 50         val = yyjson_mut_arr_iter_next(&it->it.arr);
635             }
636             }
637              
638 1026 100         if (!val) {
639 9 50         XPUSHs(&PL_sv_undef);
640 9           RETURN;
641             }
642              
643             /* return value as borrowing Doc */
644 1017           SV *result = new_doc_sv(aTHX_ it->doc, val, it->owner);
645 1017 50         XPUSHs(sv_2mortal(result));
646 1017           RETURN;
647             }
648              
649             /* pp_jkey: get current key from object iterator */
650 3           static OP * pp_jkey_impl(pTHX) {
651 3           dSP;
652 3           SV *iter_sv = POPs;
653 3           json_yy_iter_t *it = get_iter(aTHX_ iter_sv);
654              
655 3 50         if (!it->is_obj)
656 0           croak("jkey: iterator is not over an object");
657 3 50         if (!it->cur_key) {
658 0 0         XPUSHs(&PL_sv_undef);
659 0           RETURN;
660             }
661              
662 3 50         const char *kstr = yyjson_mut_get_str(it->cur_key);
663 3 50         size_t klen = yyjson_mut_get_len(it->cur_key);
664 3           SV *sv = newSVpvn(kstr, klen);
665 3 50         if (!is_ascii(kstr, klen))
666 0           SvUTF8_on(sv);
667 3 50         XPUSHs(sv_2mortal(sv));
668 3           RETURN;
669             }
670              
671             /* pp_jpatch: apply RFC 6902 JSON Patch */
672 6           static OP * pp_jpatch_impl(pTHX) {
673 6           dSP;
674 6           SV *patch_sv = POPs;
675 6           SV *doc_sv = POPs;
676 6           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
677 6 100         if (d->owner)
678 1           croak("jpatch: cannot patch a borrowed Doc; jclone it first");
679 5           json_yy_doc_t *p = get_doc(aTHX_ patch_sv);
680              
681 5           yyjson_patch_err perr = {0};
682 5           yyjson_mut_val *result = yyjson_mut_patch(d->doc, d->root, p->root, &perr);
683 5 100         if (!result)
684 1 50         croak("jpatch: %s at index %zu", perr.msg ? perr.msg : "patch failed", perr.idx);
685              
686 4 50         yyjson_mut_doc_set_root(d->doc, result);
687 4           d->root = result;
688 4 50         XPUSHs(doc_sv);
689 4           RETURN;
690             }
691              
692             /* pp_jmerge: apply RFC 7386 JSON Merge Patch */
693 2           static OP * pp_jmerge_impl(pTHX) {
694 2           dSP;
695 2           SV *patch_sv = POPs;
696 2           SV *doc_sv = POPs;
697 2           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
698 2 100         if (d->owner)
699 1           croak("jmerge: cannot merge into a borrowed Doc; jclone it first");
700 1           json_yy_doc_t *p = get_doc(aTHX_ patch_sv);
701              
702 1           yyjson_mut_val *result = yyjson_mut_merge_patch(d->doc, d->root, p->root);
703 1 50         if (!result)
704 0           croak("jmerge: merge patch failed");
705              
706 1 50         yyjson_mut_doc_set_root(d->doc, result);
707 1           d->root = result;
708 1 50         XPUSHs(doc_sv);
709 1           RETURN;
710             }
711              
712             /* pp_jfrom: create Doc from Perl data (not JSON string) */
713 5005           static OP * pp_jfrom_impl(pTHX) {
714 5005           dSP;
715 5005           SV *data = POPs;
716              
717 5005           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
718 5005 50         if (!doc) croak("jfrom: failed to create document");
719              
720             json_yy_t self_stack;
721 5005           self_stack.flags = F_UTF8 | F_ALLOW_NONREF | F_ALLOW_BLESSED;
722 5005           self_stack.max_depth = MAX_DEPTH_DEFAULT;
723              
724             /* wrap doc in a holder SV so it's freed on croak */
725 5005           SV *guard = newSV(0);
726 5005           sv_magicext(guard, NULL, PERL_MAGIC_ext, &mut_docholder_vtbl,
727             (const char *)doc, 0);
728 5005           sv_2mortal(guard);
729              
730 5005           yyjson_mut_val *root = sv_to_yyjson_val(aTHX_ doc, data, &self_stack, 0);
731             yyjson_mut_doc_set_root(doc, root);
732              
733             /* transfer doc ownership to the Doc SV; disarm the guard */
734 5005           mg_findext(guard, PERL_MAGIC_ext, &mut_docholder_vtbl)->mg_ptr = NULL;
735 5005           SV *result = new_doc_sv(aTHX_ doc, root, NULL);
736 5005 50         XPUSHs(sv_2mortal(result));
737 5005           RETURN;
738             }
739              
740             /* pp_jvals: get object values as list */
741 1           static OP * pp_jvals_impl(pTHX) {
742 1           dSP;
743 1           SV *path_sv = POPs;
744 1           SV *doc_sv = POPs;
745 1           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
746             STRLEN path_len;
747 1           const char *path = SvPVutf8(path_sv, path_len);
748              
749 1           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
750 2 50         if (!val || !yyjson_mut_is_obj(val))
    50          
751 0           croak("jvals: path does not point to an object");
752              
753             size_t idx, max;
754             yyjson_mut_val *key, *v;
755 2 50         EXTEND(SP, (SSize_t)yyjson_mut_obj_size(val));
    50          
    50          
756 5 50         yyjson_mut_obj_foreach(val, idx, max, key, v) {
    50          
    50          
    100          
757 3           SV *vsv = new_doc_sv(aTHX_ d->doc, v, doc_sv);
758 3           PUSHs(sv_2mortal(vsv));
759             }
760 1           RETURN;
761             }
762              
763             /* pp_jeq: deep equality comparison */
764 3           static OP * pp_jeq_impl(pTHX) {
765 3           dSP;
766 3           SV *b_sv = POPs;
767 3           SV *a_sv = POPs;
768 3           json_yy_doc_t *a = get_doc(aTHX_ a_sv);
769 3           json_yy_doc_t *b = get_doc(aTHX_ b_sv);
770 3 50         bool eq = yyjson_mut_equals(a->root, b->root);
771 3 50         XPUSHs(eq ? &PL_sv_yes : &PL_sv_no);
    100          
772 3           RETURN;
773             }
774              
775             /* pp_jpp: pretty-print encode */
776 1           static OP * pp_jpp_impl(pTHX) {
777 1           dSP;
778 1           SV *path_sv = POPs;
779 1           SV *doc_sv = POPs;
780 1           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
781             STRLEN path_len;
782 1           const char *path = SvPVutf8(path_sv, path_len);
783              
784 1           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
785 1 50         if (!val)
786 0           croak("jpp: path not found: %.*s", (int)path_len, path);
787              
788             size_t json_len;
789             yyjson_write_err werr;
790 1           char *json = yyjson_mut_val_write_opts(val, YYJSON_WRITE_PRETTY, NULL,
791             &json_len, &werr);
792 1 50         if (!json)
793 0           croak("jpp: write error: %s", werr.msg);
794 1           SV *result = newSVpvn(json, json_len);
795 1           free(json);
796 1 50         XPUSHs(sv_2mortal(result));
797 1           RETURN;
798             }
799              
800             /* pp_jraw: insert raw JSON string at path */
801 4           static OP * pp_jraw_impl(pTHX) {
802 4           dSP;
803 4           SV *json_sv = POPs;
804 4           SV *path_sv = POPs;
805 4           SV *doc_sv = POPs;
806 4           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
807             STRLEN path_len;
808 4           const char *path = SvPVutf8(path_sv, path_len);
809             STRLEN json_len;
810 4           const char *json = SvPVutf8(json_sv, json_len);
811              
812             /* parse the raw JSON fragment */
813 4           yyjson_doc *idoc = yyjson_read(json, json_len, YYJSON_READ_NOFLAG);
814 4 50         if (!idoc)
815 0           croak("jraw: invalid JSON fragment");
816              
817             /* copy into mutable doc */
818 4           yyjson_val *iroot = yyjson_doc_get_root(idoc);
819 4           yyjson_mut_val *new_val = yyjson_val_mut_copy(d->doc, iroot);
820             yyjson_doc_free(idoc);
821              
822 4 50         if (!new_val)
823 0           croak("jraw: failed to copy value");
824              
825 4 100         if (path_len == 0) {
826 2 100         if (d->owner)
827 1           croak("jraw: cannot replace root of a borrowed Doc; jclone it first");
828 1 50         yyjson_mut_doc_set_root(d->doc, new_val);
829 1           d->root = new_val;
830             } else {
831             yyjson_ptr_err perr;
832 2 50         bool ok = yyjson_mut_doc_ptr_setx(d->doc, path, path_len, new_val,
833             true, NULL, &perr);
834 2 100         if (!ok) {
835 1           perr = (yyjson_ptr_err){0};
836 2 50         ok = yyjson_mut_doc_ptr_addx(d->doc, path, path_len, new_val,
837             true, NULL, &perr);
838             }
839 2 50         if (!ok)
840 0 0         croak("jraw: failed to set path %.*s: %s",
841             (int)path_len, path, perr.msg ? perr.msg : "unknown error");
842             }
843              
844 3 50         XPUSHs(doc_sv);
845 3           RETURN;
846             }
847              
848             /* type predicate macros -- all follow same pattern */
849             #define PP_JIS(name, check_fn) \
850             static OP * pp_##name##_impl(pTHX) { \
851             dSP; \
852             SV *path_sv = POPs; \
853             SV *doc_sv = POPs; \
854             json_yy_doc_t *d = get_doc(aTHX_ doc_sv); \
855             STRLEN path_len; \
856             const char *path = SvPVutf8(path_sv, path_len); \
857             yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len); \
858             XPUSHs(val && check_fn(val) ? &PL_sv_yes : &PL_sv_no); \
859             RETURN; \
860             }
861              
862 2 50         static inline bool is_mut_int(yyjson_mut_val *v) {
863 3 100         return yyjson_mut_is_uint(v) || yyjson_mut_is_sint(v);
    50          
864             }
865              
866 7 50         PP_JIS(jis_obj, yyjson_mut_is_obj)
    100          
    100          
867 6 50         PP_JIS(jis_arr, yyjson_mut_is_arr)
    50          
    100          
868 5 50         PP_JIS(jis_str, yyjson_mut_is_str)
    100          
    100          
869 6 50         PP_JIS(jis_num, yyjson_mut_is_num)
    50          
    100          
870 2 50         PP_JIS(jis_int, is_mut_int)
    50          
    100          
871 4 50         PP_JIS(jis_real, yyjson_mut_is_real)
    50          
    100          
872 8 50         PP_JIS(jis_bool, yyjson_mut_is_bool)
    50          
    100          
873 4 50         PP_JIS(jis_null, yyjson_mut_is_null)
    50          
    100          
874              
875             #undef PP_JIS
876              
877             /* pp_jread: read JSON file → Doc */
878 4           static OP * pp_jread_impl(pTHX) {
879 4           dSP;
880 4           SV *path_sv = POPs;
881             STRLEN len;
882 4           const char *path = SvPV(path_sv, len);
883              
884             yyjson_read_err err;
885 4           yyjson_doc *idoc = yyjson_read_file(path, YYJSON_READ_NOFLAG, NULL, &err);
886 4 100         if (!idoc)
887 1 50         croak("jread: %s: %s", path, err.msg ? err.msg : "read failed");
888              
889 3           yyjson_mut_doc *mdoc = yyjson_doc_mut_copy(idoc, NULL);
890             yyjson_doc_free(idoc);
891 3 50         if (!mdoc)
892 0           croak("jread: failed to create mutable document");
893              
894 3           yyjson_mut_val *root = yyjson_mut_doc_get_root(mdoc);
895 3           SV *result = new_doc_sv(aTHX_ mdoc, root, NULL);
896 3 50         XPUSHs(sv_2mortal(result));
897 3           RETURN;
898             }
899              
900             /* pp_jwrite: write Doc to JSON file */
901 3           static OP * pp_jwrite_impl(pTHX) {
902 3           dSP;
903 3           SV *path_sv = POPs;
904 3           SV *doc_sv = POPs;
905 3           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
906             STRLEN len;
907 3           const char *path = SvPV(path_sv, len);
908              
909             yyjson_write_err werr;
910             /* write the subtree root, not necessarily the full doc */
911 3           yyjson_mut_doc *tmp_doc = yyjson_mut_doc_new(NULL);
912 3           yyjson_mut_val *copy = yyjson_mut_val_mut_copy(tmp_doc, d->root);
913             yyjson_mut_doc_set_root(tmp_doc, copy);
914              
915 3           bool ok = yyjson_mut_write_file(path, tmp_doc, YYJSON_WRITE_PRETTY, NULL, &werr);
916 3           yyjson_mut_doc_free(tmp_doc);
917              
918 3 50         if (!ok)
919 0 0         croak("jwrite: %s: %s", path, werr.msg ? werr.msg : "write failed");
920              
921 3 50         XPUSHs(doc_sv);
922 3           RETURN;
923             }
924              
925             /* pp_jpaths: enumerate all leaf paths */
926              
927             static void
928 14 50         collect_paths(pTHX_ yyjson_mut_val *val, SV *prefix, AV *result) {
929 14 100         if (yyjson_mut_is_obj(val)) {
930             size_t idx, max;
931             yyjson_mut_val *key, *v;
932 33 50         yyjson_mut_obj_foreach(val, idx, max, key, v) {
    100          
    100          
    100          
933 15 50         const char *kstr = yyjson_mut_get_str(key);
934 15           size_t klen = yyjson_mut_get_len(key);
935 15           SV *path = newSVsv(prefix);
936 15           sv_catpvs(path, "/");
937             /* escape ~ and / in key per RFC 6901 */
938 15           const char *p = kstr;
939 15           const char *end = kstr + klen;
940 32 100         while (p < end) {
941 17           const char *special = p;
942 54 100         while (special < end && *special != '~' && *special != '/')
    100          
    100          
943 37           special++;
944 17 50         if (special > p)
945 17           sv_catpvn(path, p, special - p);
946 17 100         if (special < end) {
947 2 100         if (*special == '~') sv_catpvs(path, "~0");
948 1           else sv_catpvs(path, "~1");
949 2           special++;
950             }
951 17           p = special;
952             }
953 26 100         if (yyjson_mut_is_obj(v) || yyjson_mut_is_arr(v)) {
    100          
954 8           collect_paths(aTHX_ v, path, result);
955 8           SvREFCNT_dec(path); /* path was used as prefix, not pushed */
956             } else {
957 7           av_push(result, path); /* transfers ownership */
958             }
959             }
960 5 100         } else if (yyjson_mut_is_arr(val)) {
961             size_t idx, max;
962             yyjson_mut_val *item;
963 16 50         yyjson_mut_arr_foreach(val, idx, max, item) {
    50          
    100          
964 4           SV *path = newSVsv(prefix);
965 4           sv_catpvs(path, "/");
966             char idxbuf[24];
967 4           int idxlen = snprintf(idxbuf, sizeof(idxbuf), "%zu", idx);
968 4           sv_catpvn(path, idxbuf, idxlen);
969 8 50         if (yyjson_mut_is_obj(item) || yyjson_mut_is_arr(item)) {
    50          
970 0           collect_paths(aTHX_ item, path, result);
971 0           SvREFCNT_dec(path);
972             } else {
973 4           av_push(result, path);
974             }
975             }
976             } else {
977             /* leaf -- the prefix itself is the path */
978 1           av_push(result, newSVsv(prefix));
979             }
980 14           }
981              
982 6           static OP * pp_jpaths_impl(pTHX) {
983 6           dSP;
984 6           SV *path_sv = POPs;
985 6           SV *doc_sv = POPs;
986 6           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
987             STRLEN path_len;
988 6           const char *path = SvPVutf8(path_sv, path_len);
989              
990 6           yyjson_mut_val *val = doc_resolve_path(aTHX_ d, path, path_len);
991 6 50         if (!val)
992 0           croak("jpaths: path not found: %.*s", (int)path_len, path);
993              
994 6           AV *paths = newAV();
995 6           SV *prefix = newSVpvn(path, path_len);
996 6           collect_paths(aTHX_ val, prefix, paths);
997 6           SvREFCNT_dec(prefix);
998              
999 6           SSize_t count = av_len(paths) + 1;
1000 6 50         EXTEND(SP, count);
    50          
1001 18 100         for (SSize_t i = 0; i < count; i++) {
1002 12           SV **svp = av_fetch(paths, i, 0);
1003 12           PUSHs(sv_2mortal(SvREFCNT_inc(*svp)));
1004             }
1005 6           SvREFCNT_dec((SV *)paths);
1006 6           RETURN;
1007             }
1008              
1009             /* pp_jfind: find first array element where key == value */
1010 8           static OP * pp_jfind_impl(pTHX) {
1011 8           dSP;
1012 8           SV *match_sv = POPs; /* value to match */
1013 8           SV *key_sv = POPs; /* key path within each element */
1014 8           SV *path_sv = POPs; /* array path */
1015 8           SV *doc_sv = POPs;
1016 8           json_yy_doc_t *d = get_doc(aTHX_ doc_sv);
1017             STRLEN path_len, key_len, match_len;
1018 8           const char *path = SvPVutf8(path_sv, path_len);
1019 8           const char *key = SvPVutf8(key_sv, key_len);
1020 8           const char *match = SvPVutf8(match_sv, match_len);
1021              
1022 8           yyjson_mut_val *arr = doc_resolve_path(aTHX_ d, path, path_len);
1023 16 50         if (!arr || !yyjson_mut_is_arr(arr)) {
    100          
1024 1 50         XPUSHs(&PL_sv_undef);
1025 1           RETURN;
1026             }
1027              
1028             size_t idx, max;
1029             yyjson_mut_val *item;
1030 30 50         yyjson_mut_arr_foreach(arr, idx, max, item) {
    50          
    100          
1031             /* look up key within this element */
1032 15           yyjson_mut_val *field = NULL;
1033 15 100         if (key_len == 0)
1034 2           field = item;
1035 13 50         else if (yyjson_mut_is_obj(item) || yyjson_mut_is_arr(item))
    0          
1036 26 50         field = yyjson_mut_ptr_getn(item, key, key_len);
1037              
1038 15 50         if (!field) continue;
1039              
1040             /* compare: string match */
1041 15 100         if (yyjson_mut_is_str(field)) {
1042 18 50         if (yyjson_mut_equals_strn(field, match, match_len)) {
    100          
1043 3           SV *result = new_doc_sv(aTHX_ d->doc, item, doc_sv);
1044 3 50         XPUSHs(sv_2mortal(result));
1045 3           RETURN;
1046             }
1047             }
1048             /* compare: number match (convert match string to number) */
1049 6 100         else if (yyjson_mut_is_num(field)) {
1050 3           NV match_nv = SvNV(match_sv);
1051 3           NV field_nv = yyjson_mut_get_num(field);
1052 3 100         if (match_nv == field_nv) {
1053 1           SV *result = new_doc_sv(aTHX_ d->doc, item, doc_sv);
1054 1 50         XPUSHs(sv_2mortal(result));
1055 1           RETURN;
1056             }
1057             }
1058             /* compare: bool/null -- match against string "true"/"false"/"null" */
1059 3 50         else if (yyjson_mut_is_bool(field)) {
1060 3           bool bval = yyjson_mut_get_bool(field);
1061 3 100         if ((bval && match_len == 4 && memcmp(match, "true", 4) == 0) ||
    100          
    50          
1062 2 100         (!bval && match_len == 5 && memcmp(match, "false", 5) == 0)) {
    50          
    50          
1063 2           SV *result = new_doc_sv(aTHX_ d->doc, item, doc_sv);
1064 2 50         XPUSHs(sv_2mortal(result));
1065 2           RETURN;
1066             }
1067             }
1068             }
1069              
1070 1 50         XPUSHs(&PL_sv_undef);
1071 1           RETURN;
1072             }
1073              
1074             /* ---- end Doc keyword ops ---- */
1075              
1076             /* check if a string is pure ASCII (no bytes >= 0x80) */
1077             static inline int
1078 4079           is_ascii(const char *s, size_t len) {
1079 4079           const unsigned char *p = (const unsigned char *)s;
1080 4079           size_t i = 0;
1081 28581 100         for (; i + 7 < len; i += 8) {
1082             uint64_t chunk;
1083 24502           memcpy(&chunk, p + i, 8);
1084 24502 50         if (chunk & UINT64_C(0x8080808080808080))
1085 0           return 0;
1086             }
1087 17135 100         for (; i < len; i++) {
1088 13065 100         if (p[i] >= 0x80)
1089 9           return 0;
1090             }
1091 4070           return 1;
1092             }
1093              
1094             /* ---- zero-copy string SV (no per-SV magic, minimal overhead) ---- */
1095             /* SvLEN=0 tells Perl it doesn't own the buffer.
1096             Perl will allocate+copy if someone does sv_setsv from this SV,
1097             so extracted values are always safe.
1098             The yyjson_doc lifetime is managed by magic on the ROOT container only. */
1099             static inline SV *
1100 1005           new_sv_zerocopy(pTHX_ const char *str, size_t len) {
1101 1005           SV *sv = newSV_type(SVt_PV);
1102 1005           SvPV_set(sv, (char *)str);
1103 1005           SvCUR_set(sv, len);
1104 1005           SvLEN_set(sv, 0);
1105 1005           SvPOK_on(sv);
1106 1005 50         if (!is_ascii(str, len))
1107 0           SvUTF8_on(sv);
1108 1005           SvREADONLY_on(sv);
1109 1005           return sv;
1110             }
1111              
1112             /* ---- DECODE: yyjson value -> Perl SV ---- */
1113              
1114             static SV *
1115 11168 50         yyjson_val_to_sv(pTHX_ yyjson_val *val) {
1116 11168           switch (yyjson_get_type(val)) {
1117 2           case YYJSON_TYPE_NULL:
1118 2           return SvREFCNT_inc_simple_NN(&PL_sv_undef);
1119              
1120 4 50         case YYJSON_TYPE_BOOL:
1121 4           return yyjson_get_bool(val)
1122 3           ? SvREFCNT_inc_simple_NN(&PL_sv_yes)
1123 4 100         : SvREFCNT_inc_simple_NN(&PL_sv_no);
1124              
1125 11023 50         case YYJSON_TYPE_NUM: {
1126 11023           yyjson_subtype st = yyjson_get_subtype(val);
1127 11023 100         if (st == YYJSON_SUBTYPE_UINT)
1128 11021           return newSVuv((UV)yyjson_get_uint(val));
1129 2 100         else if (st == YYJSON_SUBTYPE_SINT)
1130 1           return newSViv((IV)yyjson_get_sint(val));
1131             else
1132 1           return newSVnv(yyjson_get_real(val));
1133             }
1134              
1135 10 50         case YYJSON_TYPE_STR: {
1136 10 50         const char *str = yyjson_get_str(val);
1137 10           size_t len = yyjson_get_len(val);
1138 10           SV *sv = newSVpvn(str, len);
1139             /* only set UTF-8 flag if non-ASCII */
1140 10 100         if (!is_ascii(str, len))
1141 6           SvUTF8_on(sv);
1142 10           return sv;
1143             }
1144              
1145 108 50         case YYJSON_TYPE_ARR: {
1146 108           size_t count = yyjson_arr_size(val);
1147 108           AV *av = newAV();
1148 108 100         if (count > 0)
1149 107           av_extend(av, (SSize_t)count - 1);
1150 108           SV *rv = newRV_noinc((SV *)av);
1151             size_t idx, max;
1152             yyjson_val *item;
1153 20554 50         yyjson_arr_foreach(val, idx, max, item) {
    50          
    100          
1154 10115           av_push(av, yyjson_val_to_sv(aTHX_ item));
1155             }
1156 108           return rv;
1157             }
1158              
1159 21 50         case YYJSON_TYPE_OBJ: {
1160 21           size_t count = yyjson_obj_size(val);
1161 21           HV *hv = newHV();
1162 21 50         if (count > 0)
1163 21           hv_ksplit(hv, count);
1164 21           SV *rv = newRV_noinc((SV *)hv);
1165             size_t idx, max;
1166             yyjson_val *key, *value;
1167 2119 50         yyjson_obj_foreach(val, idx, max, key, value) {
    50          
    100          
1168 1028 50         const char *kstr = yyjson_get_str(key);
1169 1028           STRLEN klen = (STRLEN)yyjson_get_len(key);
1170 1028           SV *val_sv = yyjson_val_to_sv(aTHX_ value);
1171 1028 50         if (!is_ascii(kstr, klen))
1172 0           hv_store(hv, kstr, -(I32)klen, val_sv, 0);
1173             else
1174 1028           hv_store(hv, kstr, (I32)klen, val_sv, 0);
1175             }
1176 21           return rv;
1177             }
1178              
1179 0           default:
1180 0           return SvREFCNT_inc_simple_NN(&PL_sv_undef);
1181             }
1182             }
1183              
1184             /* ---- DECODE: yyjson mutable value -> Perl SV ---- */
1185              
1186             static SV *
1187 65 50         yyjson_mut_val_to_sv(pTHX_ yyjson_mut_val *val) {
1188 65           switch (yyjson_mut_get_type(val)) {
1189 1           case YYJSON_TYPE_NULL:
1190 1           return SvREFCNT_inc_simple_NN(&PL_sv_undef);
1191              
1192 2 50         case YYJSON_TYPE_BOOL:
1193 2           return yyjson_mut_get_bool(val)
1194 1           ? SvREFCNT_inc_simple_NN(&PL_sv_yes)
1195 2 100         : SvREFCNT_inc_simple_NN(&PL_sv_no);
1196              
1197 41 50         case YYJSON_TYPE_NUM: {
1198 41           yyjson_subtype st = yyjson_mut_get_subtype(val);
1199 41 100         if (st == YYJSON_SUBTYPE_UINT)
1200 34           return newSVuv((UV)yyjson_mut_get_uint(val));
1201 7 50         else if (st == YYJSON_SUBTYPE_SINT)
1202 7           return newSViv((IV)yyjson_mut_get_sint(val));
1203             else
1204 0           return newSVnv(yyjson_mut_get_real(val));
1205             }
1206              
1207 16 50         case YYJSON_TYPE_STR: {
1208 16 50         const char *str = yyjson_mut_get_str(val);
1209 16           size_t len = yyjson_mut_get_len(val);
1210 16           SV *sv = newSVpvn(str, len);
1211 16 100         if (!is_ascii(str, len))
1212 2           SvUTF8_on(sv);
1213 16           return sv;
1214             }
1215              
1216 4 50         case YYJSON_TYPE_ARR: {
1217 4           size_t count = yyjson_mut_arr_size(val);
1218 4           AV *av = newAV();
1219 4 50         if (count > 0)
1220 4           av_extend(av, (SSize_t)count - 1);
1221 4           SV *rv = newRV_noinc((SV *)av);
1222             size_t idx, max;
1223             yyjson_mut_val *item;
1224 21 50         yyjson_mut_arr_foreach(val, idx, max, item) {
    50          
    100          
1225 9           av_push(av, yyjson_mut_val_to_sv(aTHX_ item));
1226             }
1227 4           return rv;
1228             }
1229              
1230 1 50         case YYJSON_TYPE_OBJ: {
1231 1           size_t count = yyjson_mut_obj_size(val);
1232 1           HV *hv = newHV();
1233 1 50         if (count > 0)
1234 1           hv_ksplit(hv, count);
1235 1           SV *rv = newRV_noinc((SV *)hv);
1236             size_t idx, max;
1237             yyjson_mut_val *key, *value;
1238 5 50         yyjson_mut_obj_foreach(val, idx, max, key, value) {
    50          
    50          
    100          
1239 3 50         const char *kstr = yyjson_mut_get_str(key);
1240 3           STRLEN klen = (STRLEN)yyjson_mut_get_len(key);
1241 3           SV *val_sv = yyjson_mut_val_to_sv(aTHX_ value);
1242 3 50         if (!is_ascii(kstr, klen))
1243 0           hv_store(hv, kstr, -(I32)klen, val_sv, 0);
1244             else
1245 3           hv_store(hv, kstr, (I32)klen, val_sv, 0);
1246             }
1247 1           return rv;
1248             }
1249              
1250 0           default:
1251 0           return SvREFCNT_inc_simple_NN(&PL_sv_undef);
1252             }
1253             }
1254              
1255             /* ---- zero-copy readonly decoder ---- */
1256             /* doc_sv: an SV holding the yyjson_doc* (refcounted, freed on DESTROY) */
1257              
1258             static SV *
1259 3023 50         yyjson_val_to_sv_ro(pTHX_ yyjson_val *val, SV *doc_sv) {
1260 3023           switch (yyjson_get_type(val)) {
1261 1           case YYJSON_TYPE_NULL:
1262 1           return SvREFCNT_inc_simple_NN(&PL_sv_undef);
1263              
1264 3 50         case YYJSON_TYPE_BOOL:
1265 3           return yyjson_get_bool(val)
1266 2           ? SvREFCNT_inc_simple_NN(&PL_sv_yes)
1267 3 100         : SvREFCNT_inc_simple_NN(&PL_sv_no);
1268              
1269 1006 50         case YYJSON_TYPE_NUM: {
1270             SV *nsv;
1271 1006           yyjson_subtype st = yyjson_get_subtype(val);
1272 1006 50         if (st == YYJSON_SUBTYPE_UINT)
1273 1006           nsv = newSVuv((UV)yyjson_get_uint(val));
1274 0 0         else if (st == YYJSON_SUBTYPE_SINT)
1275 0           nsv = newSViv((IV)yyjson_get_sint(val));
1276             else
1277 0           nsv = newSVnv(yyjson_get_real(val));
1278 1006           SvREADONLY_on(nsv);
1279 1006           return nsv;
1280             }
1281              
1282 1005 50         case YYJSON_TYPE_STR:
1283             /* zero-copy: SV borrows string memory from yyjson_doc */
1284 2010 50         return new_sv_zerocopy(aTHX_
1285             yyjson_get_str(val), yyjson_get_len(val));
1286              
1287 3 50         case YYJSON_TYPE_ARR: {
1288 3           size_t count = yyjson_arr_size(val);
1289 3           AV *av = newAV();
1290 3 50         if (count > 0)
1291 3           av_extend(av, (SSize_t)count - 1);
1292 3           SV *rv = newRV_noinc((SV *)av);
1293             size_t idx, max;
1294             yyjson_val *item;
1295 2017 50         yyjson_arr_foreach(val, idx, max, item) {
    50          
    100          
1296 1004           av_push(av, yyjson_val_to_sv_ro(aTHX_ item, doc_sv));
1297             }
1298 3           SvREADONLY_on((SV *)av);
1299 3           return rv;
1300             }
1301              
1302 1005 50         case YYJSON_TYPE_OBJ: {
1303 1005           size_t count = yyjson_obj_size(val);
1304 1005           HV *hv = newHV();
1305 1005 50         if (count > 0)
1306 1005           hv_ksplit(hv, count);
1307 1005           SV *rv = newRV_noinc((SV *)hv);
1308             size_t idx, max;
1309             yyjson_val *key, *value;
1310 7033 50         yyjson_obj_foreach(val, idx, max, key, value) {
    50          
    100          
1311 2009 50         const char *kstr = yyjson_get_str(key);
1312 2009           STRLEN klen = (STRLEN)yyjson_get_len(key);
1313 2009           SV *val_sv = yyjson_val_to_sv_ro(aTHX_ value, doc_sv);
1314 2009 50         if (!is_ascii(kstr, klen))
1315 0           hv_store(hv, kstr, -(I32)klen, val_sv, 0);
1316             else
1317 2009           hv_store(hv, kstr, (I32)klen, val_sv, 0);
1318             }
1319 1005           SvREADONLY_on((SV *)hv);
1320 1005           return rv;
1321             }
1322              
1323 0           default:
1324 0           return SvREFCNT_inc_simple_NN(&PL_sv_undef);
1325             }
1326             }
1327              
1328             /* create a doc-holder SV: an opaque SV that frees yyjson_doc when destroyed */
1329             static SV *
1330 10           new_doc_holder(pTHX_ yyjson_doc *doc) {
1331 10           SV *sv = newSV(0);
1332 10           sv_magicext(sv, NULL, PERL_MAGIC_ext, &docholder_magic_vtbl,
1333             (const char *)doc, 0);
1334 10           return sv;
1335             }
1336              
1337             /* ---- DIRECT ENCODE: single-pass SV -> JSON bytes ---- */
1338             /* Bypasses yyjson_mut_doc entirely for maximum throughput */
1339              
1340             /* escape table: 0 = passthrough, 1+ = needs escaping */
1341             static const uint8_t escape_table[256] = {
1342             /* 0x00-0x1f: control characters need \uXXXX */
1343             1,1,1,1,1,1,1,1, 'b','t','n',1,'f','r',1,1,
1344             1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,
1345             /* 0x20-0x7f */
1346             0,0,'"',0,0,0,0,0, 0,0,0,0,0,0,0,0, /* " at 0x22 */
1347             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, /* 0x30-0x3f */
1348             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, /* 0x40-0x4f */
1349             0,0,0,0,0,0,0,0, 0,0,0,0,'\\',0,0,0, /* \\ at 0x5c */
1350             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1351             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1352             /* 0x80-0xff: high bytes, pass through (valid UTF-8) */
1353             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1354             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1355             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1356             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1357             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1358             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1359             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1360             0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
1361             };
1362              
1363             /* ensure buf SV has room for `need` more bytes */
1364             static inline void
1365 16327           buf_ensure(pTHX_ SV *buf, size_t need) {
1366 16327           STRLEN cur = SvCUR(buf);
1367 16327           STRLEN avail = SvLEN(buf) - cur - 1;
1368 16327 100         if (avail < need) {
1369 20           STRLEN newlen = (cur + need + 1) * 2;
1370 20 50         SvGROW(buf, newlen);
    50          
1371             }
1372 16327           }
1373              
1374             static inline void
1375 10198           buf_cat_c(pTHX_ SV *buf, char c) {
1376 10198           buf_ensure(aTHX_ buf, 1);
1377 10198           char *p = SvPVX(buf) + SvCUR(buf);
1378 10198           *p = c;
1379 10198           SvCUR_set(buf, SvCUR(buf) + 1);
1380 10198           }
1381              
1382             static inline void
1383 2028           buf_cat_mem(pTHX_ SV *buf, const char *s, size_t n) {
1384 2028           buf_ensure(aTHX_ buf, n);
1385 2028           char *p = SvPVX(buf) + SvCUR(buf);
1386 2028           memcpy(p, s, n);
1387 2028           SvCUR_set(buf, SvCUR(buf) + n);
1388 2028           }
1389              
1390             /* check if string needs any escaping */
1391             static inline int
1392 4100           needs_escape(const char *s, size_t len) {
1393             /* check 8 bytes at a time for common case (no control chars, no " or \) */
1394             /* bytes needing escape: 0x00-0x1f, 0x22 ("), 0x5c (\) */
1395 4100           const unsigned char *p = (const unsigned char *)s;
1396 4100           size_t i = 0;
1397 28600 100         for (; i + 7 < len; i += 8) {
1398             /* any byte < 0x20? */
1399             uint64_t chunk;
1400 24533           memcpy(&chunk, p + i, 8);
1401             /* any byte < 0x20? subtract 0x20 from each byte; underflow sets high bit */
1402 24533 100         if ((chunk - UINT64_C(0x2020202020202020)) & ~chunk & UINT64_C(0x8080808080808080))
1403 33           return 1;
1404             /* check for " (0x22) or \ (0x5c) byte by byte in chunk */
1405 24500           uint64_t xor_quote = chunk ^ UINT64_C(0x2222222222222222);
1406 24500           uint64_t xor_bslash = chunk ^ UINT64_C(0x5c5c5c5c5c5c5c5c);
1407             /* a byte is zero iff (v - 0x01) & ~v & 0x80 */
1408             #define HAS_ZERO(v) (((v) - UINT64_C(0x0101010101010101)) & ~(v) & UINT64_C(0x8080808080808080))
1409 24500 50         if (HAS_ZERO(xor_quote) || HAS_ZERO(xor_bslash))
    50          
1410 0           return 1;
1411             #undef HAS_ZERO
1412             }
1413 15179 100         for (; i < len; i++) {
1414 11112 50         if (escape_table[p[i]])
1415 0           return 1;
1416             }
1417 4067           return 0;
1418             }
1419              
1420             static void
1421 4100           buf_cat_escaped_str(pTHX_ SV *buf, const char *s, size_t len) {
1422             /* fast path: no escaping needed (very common for JSON keys/values) */
1423 4100 100         if (!needs_escape(s, len)) {
1424 4067           buf_ensure(aTHX_ buf, len + 2);
1425 4067           char *out = SvPVX(buf) + SvCUR(buf);
1426 4067           *out++ = '"';
1427 4067           memcpy(out, s, len);
1428 4067           out += len;
1429 4067           *out++ = '"';
1430 4067           SvCUR_set(buf, out - SvPVX(buf));
1431 4067           return;
1432             }
1433              
1434             /* slow path: need escaping */
1435             static const char hex_digits[] = "0123456789abcdef";
1436 33           buf_ensure(aTHX_ buf, len + 2 + 16); /* some headroom */
1437 33           char *out = SvPVX(buf) + SvCUR(buf);
1438 33           char *out_end = SvPVX(buf) + SvLEN(buf) - 1;
1439 33           *out++ = '"';
1440              
1441 33           const char *end = s + len;
1442 132 100         while (s < end) {
1443             /* ensure we have room for at least one escaped char */
1444 99 50         if (out + 8 > out_end) {
1445 0           SvCUR_set(buf, out - SvPVX(buf));
1446 0           buf_ensure(aTHX_ buf, (end - s) * 2 + 8);
1447 0           out = SvPVX(buf) + SvCUR(buf);
1448 0           out_end = SvPVX(buf) + SvLEN(buf) - 1;
1449             }
1450              
1451 99           unsigned char c = *s;
1452 99           uint8_t esc = escape_table[c];
1453 99 100         if (!esc) {
1454             /* scan for run of safe chars */
1455 66           const char *safe = s + 1;
1456 399 100         while (safe < end && !escape_table[(unsigned char)*safe])
    100          
1457 333           safe++;
1458 66           size_t n = safe - s;
1459 66 50         if (out + n > out_end) {
1460 0           SvCUR_set(buf, out - SvPVX(buf));
1461 0           buf_ensure(aTHX_ buf, n + (end - safe) * 2 + 8);
1462 0           out = SvPVX(buf) + SvCUR(buf);
1463 0           out_end = SvPVX(buf) + SvLEN(buf) - 1;
1464             }
1465 66           memcpy(out, s, n);
1466 66           out += n;
1467 66           s = safe;
1468 33 100         } else if (esc > 1) {
1469 5           *out++ = '\\';
1470 5           *out++ = (char)esc;
1471 5           s++;
1472             } else {
1473 28           *out++ = '\\'; *out++ = 'u'; *out++ = '0'; *out++ = '0';
1474 28           *out++ = hex_digits[c >> 4];
1475 28           *out++ = hex_digits[c & 0x0f];
1476 28           s++;
1477             }
1478             }
1479 33           *out++ = '"';
1480 33           SvCUR_set(buf, out - SvPVX(buf));
1481             }
1482              
1483             /* fast unsigned integer to buffer */
1484             static void
1485 2025           buf_cat_uv(pTHX_ SV *buf, UV val) {
1486             char tmp[24];
1487 2025           char *p = tmp + sizeof(tmp);
1488 2025 50         if (val == 0) {
1489 0           *--p = '0';
1490             } else {
1491 7839 100         while (val) {
1492 5814           *--p = '0' + (val % 10);
1493 5814           val /= 10;
1494             }
1495             }
1496 2025           buf_cat_mem(aTHX_ buf, p, (tmp + sizeof(tmp)) - p);
1497 2025           }
1498              
1499             static void
1500 2025           buf_cat_iv(pTHX_ SV *buf, IV val) {
1501 2025 100         if (val < 0) {
1502 1           buf_cat_c(aTHX_ buf, '-');
1503             /* handle IV_MIN carefully */
1504 1           buf_cat_uv(aTHX_ buf, (UV)(-(val + 1)) + 1);
1505             } else {
1506 2024           buf_cat_uv(aTHX_ buf, (UV)val);
1507             }
1508 2025           }
1509              
1510             static void
1511 1           buf_cat_nv(pTHX_ SV *buf, NV val) {
1512 1           buf_ensure(aTHX_ buf, 40);
1513 1           char *p = SvPVX(buf) + SvCUR(buf);
1514 1           Gconvert(val, NV_DIG, 0, p);
1515 1           int len = strlen(p);
1516 1           SvCUR_set(buf, SvCUR(buf) + len);
1517 1           }
1518              
1519             static json_yy_t default_self = { F_UTF8 | F_ALLOW_NONREF, MAX_DEPTH_DEFAULT };
1520              
1521             static void
1522 5138           direct_encode_sv(pTHX_ SV *buf, SV *sv, U32 depth, json_yy_t *self) {
1523 5138 100         if (depth > self->max_depth)
1524 2           croak("maximum nesting depth exceeded");
1525              
1526 5136 100         if (!SvOK(sv)) {
1527 1           buf_cat_mem(aTHX_ buf, "null", 4);
1528 1           return;
1529             }
1530              
1531 5135 100         if (SvROK(sv)) {
1532 2067           SV *deref = SvRV(sv);
1533              
1534 2067 100         if (SvOBJECT(deref)) {
1535 2 100         if (self->flags & F_CONVERT_BLESSED) {
1536 1           dSP;
1537 1           ENTER; SAVETMPS;
1538 1 50         PUSHMARK(SP);
1539 1 50         XPUSHs(sv);
1540 1           PUTBACK;
1541 1           int count = call_method("TO_JSON", G_SCALAR | G_EVAL);
1542 1           SPAGAIN;
1543 1 50         if (SvTRUE(ERRSV)) {
    50          
1544 0 0         SV *err = ERRSV;
1545 0 0         PUTBACK; FREETMPS; LEAVE;
1546 0           croak("TO_JSON method failed: %" SVf, SVfARG(err));
1547             }
1548 1 50         SV *result = count > 0 ? POPs : &PL_sv_undef;
1549 1           SvREFCNT_inc(result);
1550 1 50         PUTBACK; FREETMPS; LEAVE;
1551 1           direct_encode_sv(aTHX_ buf, result, depth, self);
1552 1           SvREFCNT_dec(result);
1553 1           return;
1554             }
1555 1 50         if (self->flags & F_ALLOW_BLESSED) {
1556 1           buf_cat_mem(aTHX_ buf, "null", 4);
1557 1           return;
1558             }
1559 0           croak("encountered object '%s', but allow_blessed/convert_blessed is not enabled",
1560             sv_reftype(deref, 1));
1561             }
1562              
1563             /* scalar ref: boolean */
1564 2065 100         if (SvTYPE(deref) < SVt_PVAV) {
1565 1 50         if (SvTRUE(deref))
1566 1           buf_cat_mem(aTHX_ buf, "true", 4);
1567             else
1568 0           buf_cat_mem(aTHX_ buf, "false", 5);
1569 1           return;
1570             }
1571              
1572 2064 100         if (SvTYPE(deref) == SVt_PVAV) {
1573 10           AV *av = (AV *)deref;
1574 10           SSize_t len = av_len(av) + 1;
1575 10           buf_cat_c(aTHX_ buf, '[');
1576 2027 100         for (SSize_t i = 0; i < len; i++) {
1577 2017 100         if (i) buf_cat_c(aTHX_ buf, ',');
1578 2017           SV **elem = av_fetch(av, i, 0);
1579 2017 50         direct_encode_sv(aTHX_ buf, elem ? *elem : &PL_sv_undef,
1580             depth + 1, self);
1581             }
1582 10           buf_cat_c(aTHX_ buf, ']');
1583 10           return;
1584             }
1585              
1586 2054 50         if (SvTYPE(deref) == SVt_PVHV) {
1587 2054           HV *hv = (HV *)deref;
1588 2054           buf_cat_c(aTHX_ buf, '{');
1589 2054           hv_iterinit(hv);
1590             HE *he;
1591 2054           int first = 1;
1592 5108 100         while ((he = hv_iternext(hv))) {
1593 3061 100         if (!first) buf_cat_c(aTHX_ buf, ',');
1594 3061           first = 0;
1595             STRLEN klen;
1596 3061 50         const char *kstr = HePV(he, klen);
1597 3061           buf_cat_escaped_str(aTHX_ buf, kstr, klen);
1598 3061           buf_cat_c(aTHX_ buf, ':');
1599 3061           direct_encode_sv(aTHX_ buf, HeVAL(he), depth + 1, self);
1600             }
1601 2047           buf_cat_c(aTHX_ buf, '}');
1602 2047           return;
1603             }
1604              
1605 0 0         if (self->flags & F_ALLOW_UNKNOWN) {
1606 0           buf_cat_mem(aTHX_ buf, "null", 4);
1607 0           return;
1608             }
1609 0           croak("cannot encode reference to %s", sv_reftype(deref, 0));
1610             }
1611              
1612 3068 100         if (SvIOK(sv)) {
1613 2025 50         if (SvIsUV(sv))
1614 0           buf_cat_uv(aTHX_ buf, SvUVX(sv));
1615             else
1616 2025           buf_cat_iv(aTHX_ buf, SvIVX(sv));
1617 2025           return;
1618             }
1619              
1620 1043 100         if (SvNOK(sv)) {
1621 4           NV nv = SvNVX(sv);
1622 4 100         if (Perl_isnan(nv) || Perl_isinf(nv))
    100          
1623 3           croak("cannot encode NaN or Infinity as JSON");
1624 1           buf_cat_nv(aTHX_ buf, nv);
1625 1           return;
1626             }
1627              
1628 1039 50         if (SvPOK(sv)) {
1629             STRLEN len;
1630 1039           const char *str = SvPV(sv, len);
1631 1039           buf_cat_escaped_str(aTHX_ buf, str, len);
1632 1039           return;
1633             }
1634              
1635 0           buf_cat_mem(aTHX_ buf, "null", 4);
1636             }
1637              
1638             /* ---- ENCODE: Perl SV -> yyjson mutable value (used for OO API) ---- */
1639              
1640             static yyjson_mut_val *
1641 45049           sv_to_yyjson_val(pTHX_ yyjson_mut_doc *doc, SV *sv, json_yy_t *self, U32 depth) {
1642 45049 50         if (depth > self->max_depth)
1643 0           croak("maximum nesting depth exceeded");
1644              
1645 45049 100         if (!SvOK(sv))
1646 1           return yyjson_mut_null(doc);
1647              
1648 45048 100         if (SvROK(sv)) {
1649 15015           SV *deref = SvRV(sv);
1650              
1651             /* check for blessed objects */
1652 15015 50         if (SvOBJECT(deref)) {
1653             /* convert_blessed: call TO_JSON */
1654 0 0         if (self->flags & F_CONVERT_BLESSED) {
1655 0           dSP;
1656 0           ENTER; SAVETMPS;
1657 0 0         PUSHMARK(SP);
1658 0 0         XPUSHs(sv);
1659 0           PUTBACK;
1660 0           int count = call_method("TO_JSON", G_SCALAR | G_EVAL);
1661 0           SPAGAIN;
1662 0 0         if (SvTRUE(ERRSV)) {
    0          
1663 0 0         SV *err = ERRSV;
1664 0 0         PUTBACK; FREETMPS; LEAVE;
1665 0           croak("TO_JSON method failed: %" SVf, SVfARG(err));
1666             }
1667 0 0         SV *result = count > 0 ? POPs : &PL_sv_undef;
1668 0           SvREFCNT_inc(result);
1669 0 0         PUTBACK; FREETMPS; LEAVE;
1670 0           yyjson_mut_val *ret = sv_to_yyjson_val(aTHX_ doc, result, self, depth);
1671 0           SvREFCNT_dec(result);
1672 0           return ret;
1673             }
1674             /* allow_blessed: encode as null */
1675 0 0         if (self->flags & F_ALLOW_BLESSED)
1676 0           return yyjson_mut_null(doc);
1677 0           croak("encountered object '%s', but allow_blessed/convert_blessed is not enabled",
1678             sv_reftype(deref, 1));
1679             }
1680              
1681             /* scalar ref: \1 = true, \0 = false */
1682 15015 100         if (SvTYPE(deref) < SVt_PVAV) {
1683 1           return SvTRUE(deref)
1684 1           ? yyjson_mut_bool(doc, 1)
1685 2 50         : yyjson_mut_bool(doc, 0);
1686             }
1687              
1688 15014           switch (SvTYPE(deref)) {
1689 5003           case SVt_PVAV: {
1690 5003 50         AV *av = (AV *)deref;
1691 5003           yyjson_mut_val *arr = yyjson_mut_arr(doc);
1692 5003           SSize_t len = av_len(av) + 1;
1693 20012 100         for (SSize_t i = 0; i < len; i++) {
1694 15009           SV **elem = av_fetch(av, i, 0);
1695 15009 50         yyjson_mut_val *v = sv_to_yyjson_val(aTHX_ doc, elem ? *elem : &PL_sv_undef, self, depth + 1);
1696             yyjson_mut_arr_append(arr, v);
1697             }
1698 5003           return arr;
1699             }
1700              
1701 10011           case SVt_PVHV: {
1702 10011 50         HV *hv = (HV *)deref;
1703 10011           yyjson_mut_val *obj = yyjson_mut_obj(doc);
1704 10011           hv_iterinit(hv);
1705             HE *he;
1706 25026 100         while ((he = hv_iternext(hv))) {
1707             STRLEN klen;
1708 15015 50         const char *kstr = HePV(he, klen);
1709 15015           SV *val = HeVAL(he);
1710 15015 50         yyjson_mut_val *k = yyjson_mut_strncpy(doc, kstr, klen);
1711 15015           yyjson_mut_val *v = sv_to_yyjson_val(aTHX_ doc, val, self, depth + 1);
1712             yyjson_mut_obj_add(obj, k, v);
1713             }
1714 10011           return obj;
1715             }
1716              
1717 0           default:
1718 0 0         if (self->flags & F_ALLOW_UNKNOWN)
1719 0           return yyjson_mut_null(doc);
1720 0           croak("cannot encode reference to %s", sv_reftype(deref, 0));
1721             }
1722             }
1723              
1724             /* check for boolean (JSON::PP::Boolean, Types::Serialiser::Boolean, etc.) */
1725             /* SvIOK first for speed */
1726 30033 100         if (SvIOK(sv)) {
1727 25026 50         if (SvIsUV(sv))
1728 0 0         return yyjson_mut_uint(doc, (uint64_t)SvUVX(sv));
1729 50052 50         return yyjson_mut_sint(doc, (int64_t)SvIVX(sv));
1730             }
1731              
1732 5007 100         if (SvNOK(sv)) {
1733 2           NV nv = SvNVX(sv);
1734 2 50         if (Perl_isnan(nv) || Perl_isinf(nv))
    50          
1735 2           croak("cannot encode NaN or Infinity as JSON");
1736 0           return yyjson_mut_real(doc, nv);
1737             }
1738              
1739 5005 50         if (SvPOK(sv)) {
1740             STRLEN len;
1741 5005           const char *str = SvPV(sv, len);
1742 10010 50         return yyjson_mut_strncpy(doc, str, len);
1743             }
1744              
1745 0           return yyjson_mut_null(doc);
1746             }
1747              
1748             /* ---- custom ops for keyword API ---- */
1749              
1750             /* pp function for decode_json keyword */
1751             static OP *
1752 23           pp_decode_json_impl(pTHX) {
1753 23           dSP;
1754 23           SV *json_sv = POPs;
1755             STRLEN len;
1756 23           const char *json = SvPV(json_sv, len);
1757              
1758             yyjson_read_err err;
1759 23           yyjson_doc *doc = yyjson_read_opts((char *)json, len, YYJSON_READ_NOFLAG, NULL, &err);
1760 23 100         if (!doc)
1761 3           croak("JSON decode error: %s at byte offset %zu", err.msg, err.pos);
1762              
1763 20           yyjson_val *root = yyjson_doc_get_root(doc);
1764 20 50         if (!root) {
1765             yyjson_doc_free(doc);
1766 0           croak("JSON decode error: empty document");
1767             }
1768              
1769 20           SV *result = yyjson_val_to_sv(aTHX_ root);
1770             yyjson_doc_free(doc);
1771              
1772 20 50         XPUSHs(sv_2mortal(result));
1773 20           RETURN;
1774             }
1775              
1776             /* pp function for encode_json keyword */
1777             static OP *
1778 50           pp_encode_json_impl(pTHX) {
1779 50           dSP;
1780 50           SV *data = POPs;
1781              
1782 50           SV *result = newSV(64);
1783 50           SvPOK_on(result);
1784 50           SvCUR_set(result, 0);
1785 50           direct_encode_sv(aTHX_ result, data, 0, &default_self);
1786 47           *(SvPVX(result) + SvCUR(result)) = '\0';
1787              
1788 47 50         XPUSHs(sv_2mortal(result));
1789 47           RETURN;
1790             }
1791              
1792             /* pp function for decode_json_ro keyword */
1793             static OP *
1794 11           pp_decode_json_ro_impl(pTHX) {
1795 11           dSP;
1796 11           SV *json_sv = POPs;
1797             STRLEN len;
1798 11           const char *json = SvPV(json_sv, len);
1799              
1800             yyjson_read_err err;
1801 11           yyjson_doc *doc = yyjson_read_opts((char *)json, len, YYJSON_READ_NOFLAG, NULL, &err);
1802 11 100         if (!doc)
1803 1           croak("JSON decode error: %s at byte offset %zu", err.msg, err.pos);
1804              
1805 10           yyjson_val *root = yyjson_doc_get_root(doc);
1806 10 50         if (!root) {
1807             yyjson_doc_free(doc);
1808 0           croak("JSON decode error: empty document");
1809             }
1810              
1811 10           SV *doc_sv = new_doc_holder(aTHX_ doc);
1812 10           SV *result = yyjson_val_to_sv_ro(aTHX_ root, doc_sv);
1813              
1814             /* attach doc_sv to keep yyjson_doc alive while zero-copy SVs exist.
1815             skip for null/bool roots -- they return immortal globals that must
1816             not accumulate magic. */
1817 10           yyjson_type rtype = yyjson_get_type(root);
1818 10 100         if (rtype != YYJSON_TYPE_NULL && rtype != YYJSON_TYPE_BOOL) {
    100          
1819 7 100         SV *anchor = SvROK(result) ? SvRV(result) : result;
1820 7           sv_magicext(anchor, doc_sv, PERL_MAGIC_ext, &empty_vtbl, NULL, 0);
1821             }
1822 10           SvREFCNT_dec(doc_sv);
1823              
1824 10 50         XPUSHs(sv_2mortal(result));
1825 10           RETURN;
1826             }
1827              
1828             /* ---- XS::Parse::Keyword op builders ---- */
1829              
1830             static OP *
1831 165           make_custom_unop(pTHX_ Perl_ppaddr_t ppfunc, OP *arg) {
1832 165           OP *o = newUNOP(OP_NULL, 0, arg);
1833 165           o->op_type = OP_CUSTOM;
1834 165           o->op_ppaddr = ppfunc;
1835 165           return o;
1836             }
1837              
1838             static OP *
1839 181           make_custom_binop(pTHX_ Perl_ppaddr_t ppfunc, OP *a, OP *b) {
1840 181           OP *o = newBINOP(OP_NULL, 0, a, b);
1841 181           o->op_type = OP_CUSTOM;
1842 181           o->op_ppaddr = ppfunc;
1843 181           return o;
1844             }
1845              
1846             static OP *
1847 28           make_custom_3op(pTHX_ Perl_ppaddr_t ppfunc, OP *a, OP *b, OP *c) {
1848 28           OP *ab = newBINOP(OP_NULL, 0, a, b);
1849 28           OP *o = newBINOP(OP_NULL, 0, ab, c);
1850 28           o->op_type = OP_CUSTOM;
1851 28           o->op_ppaddr = ppfunc;
1852 28           return o;
1853             }
1854              
1855             static OP *
1856 8           make_custom_4op(pTHX_ Perl_ppaddr_t ppfunc, OP *a, OP *b, OP *c, OP *d) {
1857 8           OP *ab = newBINOP(OP_NULL, 0, a, b);
1858 8           OP *cd = newBINOP(OP_NULL, 0, c, d);
1859 8           OP *o = newBINOP(OP_NULL, 0, ab, cd);
1860 8           o->op_type = OP_CUSTOM;
1861 8           o->op_ppaddr = ppfunc;
1862 8           return o;
1863             }
1864              
1865             /* ---- XS::Parse::Keyword hooks ---- */
1866              
1867             /* macro to define build callback + hooks for 0-arg keyword */
1868             #define XPK_KW0(name, ppfunc) \
1869             static int build_kw_##name(pTHX_ OP **out, XSParseKeywordPiece *args[], \
1870             size_t nargs, void *hookdata) { \
1871             PERL_UNUSED_ARG(args); PERL_UNUSED_ARG(nargs); PERL_UNUSED_ARG(hookdata); \
1872             OP *o = newOP(OP_NULL, 0); o->op_type = OP_CUSTOM; o->op_ppaddr = ppfunc; \
1873             *out = o; return KEYWORD_PLUGIN_EXPR; \
1874             } \
1875             static const struct XSParseKeywordHooks hooks_##name = { \
1876             .permit_hintkey = "JSON::YY/" #name, \
1877             .pieces = (const struct XSParseKeywordPieceType []){ {0} }, \
1878             .build = &build_kw_##name, \
1879             };
1880              
1881             /* macro for 1-arg keyword */
1882             #define XPK_KW1(name, ppfunc) \
1883             static int build_kw_##name(pTHX_ OP **out, XSParseKeywordPiece *args[], \
1884             size_t nargs, void *hookdata) { \
1885             PERL_UNUSED_ARG(nargs); PERL_UNUSED_ARG(hookdata); \
1886             *out = make_custom_unop(aTHX_ ppfunc, args[0]->op); \
1887             return KEYWORD_PLUGIN_EXPR; \
1888             } \
1889             static const struct XSParseKeywordHooks hooks_##name = { \
1890             .permit_hintkey = "JSON::YY/" #name, \
1891             .pieces = (const struct XSParseKeywordPieceType []){ XPK_TERMEXPR, {0} }, \
1892             .build = &build_kw_##name, \
1893             };
1894              
1895             /* macro for 2-arg keyword */
1896             #define XPK_KW2(name, ppfunc) \
1897             static int build_kw_##name(pTHX_ OP **out, XSParseKeywordPiece *args[], \
1898             size_t nargs, void *hookdata) { \
1899             PERL_UNUSED_ARG(nargs); PERL_UNUSED_ARG(hookdata); \
1900             *out = make_custom_binop(aTHX_ ppfunc, args[0]->op, args[1]->op); \
1901             return KEYWORD_PLUGIN_EXPR; \
1902             } \
1903             static const struct XSParseKeywordHooks hooks_##name = { \
1904             .permit_hintkey = "JSON::YY/" #name, \
1905             .pieces = (const struct XSParseKeywordPieceType []){ \
1906             XPK_TERMEXPR, XPK_COMMA, XPK_TERMEXPR, {0} }, \
1907             .build = &build_kw_##name, \
1908             };
1909              
1910             /* macro for 3-arg keyword */
1911             #define XPK_KW3(name, ppfunc) \
1912             static int build_kw_##name(pTHX_ OP **out, XSParseKeywordPiece *args[], \
1913             size_t nargs, void *hookdata) { \
1914             PERL_UNUSED_ARG(nargs); PERL_UNUSED_ARG(hookdata); \
1915             *out = make_custom_3op(aTHX_ ppfunc, args[0]->op, args[1]->op, args[2]->op); \
1916             return KEYWORD_PLUGIN_EXPR; \
1917             } \
1918             static const struct XSParseKeywordHooks hooks_##name = { \
1919             .permit_hintkey = "JSON::YY/" #name, \
1920             .pieces = (const struct XSParseKeywordPieceType []){ \
1921             XPK_TERMEXPR, XPK_COMMA, XPK_TERMEXPR, XPK_COMMA, XPK_TERMEXPR, {0} }, \
1922             .build = &build_kw_##name, \
1923             };
1924              
1925             /* macro for 4-arg keyword */
1926             #define XPK_KW4(name, ppfunc) \
1927             static int build_kw_##name(pTHX_ OP **out, XSParseKeywordPiece *args[], \
1928             size_t nargs, void *hookdata) { \
1929             PERL_UNUSED_ARG(nargs); PERL_UNUSED_ARG(hookdata); \
1930             *out = make_custom_4op(aTHX_ ppfunc, args[0]->op, args[1]->op, \
1931             args[2]->op, args[3]->op); \
1932             return KEYWORD_PLUGIN_EXPR; \
1933             } \
1934             static const struct XSParseKeywordHooks hooks_##name = { \
1935             .permit_hintkey = "JSON::YY/" #name, \
1936             .pieces = (const struct XSParseKeywordPieceType []){ \
1937             XPK_TERMEXPR, XPK_COMMA, XPK_TERMEXPR, XPK_COMMA, \
1938             XPK_TERMEXPR, XPK_COMMA, XPK_TERMEXPR, {0} }, \
1939             .build = &build_kw_##name, \
1940             };
1941              
1942             /* functional API */
1943 23           XPK_KW1(encode_json, pp_encode_json_impl)
1944 28           XPK_KW1(decode_json, pp_decode_json_impl)
1945 11           XPK_KW1(decode_json_ro, pp_decode_json_ro_impl)
1946              
1947             /* doc creation */
1948 72           XPK_KW1(jdoc, pp_jdoc_impl)
1949 6           XPK_KW1(jfrom, pp_jfrom_impl)
1950 4           XPK_KW1(jread, pp_jread_impl)
1951              
1952             /* value constructors */
1953 4           XPK_KW1(jstr, pp_jstr_impl)
1954 3           XPK_KW1(jnum, pp_jnum_impl)
1955 4           XPK_KW1(jbool, pp_jbool_impl)
1956 2           XPK_KW0(jnull, pp_jnull_impl)
1957 3           XPK_KW0(jarr, pp_jarr_impl)
1958 2           XPK_KW0(jobj, pp_jobj_impl)
1959              
1960             /* path ops */
1961 8           XPK_KW2(jget, pp_jget_impl)
1962 44           XPK_KW2(jgetp, pp_jgetp_impl)
1963 24           XPK_KW3(jset, pp_jset_impl)
1964 5           XPK_KW2(jdel, pp_jdel_impl)
1965 8           XPK_KW2(jhas, pp_jhas_impl)
1966 4           XPK_KW2(jclone, pp_jclone_impl)
1967 3           XPK_KW2(jwrite, pp_jwrite_impl)
1968 35           XPK_KW2(jencode, pp_jencode_impl)
1969 1           XPK_KW2(jpp, pp_jpp_impl)
1970 4           XPK_KW3(jraw, pp_jraw_impl)
1971              
1972             /* inspection */
1973 8           XPK_KW2(jtype, pp_jtype_impl)
1974 9           XPK_KW2(jlen, pp_jlen_impl)
1975 4           XPK_KW2(jkeys, pp_jkeys_impl)
1976 1           XPK_KW2(jvals, pp_jvals_impl)
1977 6           XPK_KW2(jpaths, pp_jpaths_impl)
1978 8           XPK_KW4(jfind, pp_jfind_impl)
1979              
1980             /* iteration */
1981 10           XPK_KW2(jiter, pp_jiter_impl)
1982 9           XPK_KW1(jnext, pp_jnext_impl)
1983 1           XPK_KW1(jkey, pp_jkey_impl)
1984              
1985             /* patching */
1986 6           XPK_KW2(jpatch, pp_jpatch_impl)
1987 2           XPK_KW2(jmerge, pp_jmerge_impl)
1988              
1989             /* comparison */
1990 3           XPK_KW2(jeq, pp_jeq_impl)
1991              
1992             /* type predicates */
1993 4           XPK_KW2(jis_obj, pp_jis_obj_impl)
1994 3           XPK_KW2(jis_arr, pp_jis_arr_impl)
1995 3           XPK_KW2(jis_str, pp_jis_str_impl)
1996 3           XPK_KW2(jis_num, pp_jis_num_impl)
1997 2           XPK_KW2(jis_int, pp_jis_int_impl)
1998 2           XPK_KW2(jis_real, pp_jis_real_impl)
1999 4           XPK_KW2(jis_bool, pp_jis_bool_impl)
2000 2           XPK_KW2(jis_null, pp_jis_null_impl)
2001              
2002             /* alias: jdecode = jgetp */
2003 1           XPK_KW2(jdecode, pp_jgetp_impl)
2004              
2005             MODULE = JSON::YY PACKAGE = JSON::YY
2006              
2007             BOOT:
2008             {
2009 14           boot_xs_parse_keyword(0.40);
2010              
2011             /* functional API keywords */
2012 14           register_xs_parse_keyword("encode_json", &hooks_encode_json, NULL);
2013 14           register_xs_parse_keyword("decode_json", &hooks_decode_json, NULL);
2014 14           register_xs_parse_keyword("decode_json_ro", &hooks_decode_json_ro, NULL);
2015              
2016             /* doc creation */
2017 14           register_xs_parse_keyword("jdoc", &hooks_jdoc, NULL);
2018 14           register_xs_parse_keyword("jfrom", &hooks_jfrom, NULL);
2019 14           register_xs_parse_keyword("jread", &hooks_jread, NULL);
2020              
2021             /* value constructors */
2022 14           register_xs_parse_keyword("jstr", &hooks_jstr, NULL);
2023 14           register_xs_parse_keyword("jnum", &hooks_jnum, NULL);
2024 14           register_xs_parse_keyword("jbool", &hooks_jbool, NULL);
2025 14           register_xs_parse_keyword("jnull", &hooks_jnull, NULL);
2026 14           register_xs_parse_keyword("jarr", &hooks_jarr, NULL);
2027 14           register_xs_parse_keyword("jobj", &hooks_jobj, NULL);
2028              
2029             /* path operations */
2030 14           register_xs_parse_keyword("jget", &hooks_jget, NULL);
2031 14           register_xs_parse_keyword("jgetp", &hooks_jgetp, NULL);
2032 14           register_xs_parse_keyword("jset", &hooks_jset, NULL);
2033 14           register_xs_parse_keyword("jdel", &hooks_jdel, NULL);
2034 14           register_xs_parse_keyword("jhas", &hooks_jhas, NULL);
2035 14           register_xs_parse_keyword("jclone", &hooks_jclone, NULL);
2036 14           register_xs_parse_keyword("jwrite", &hooks_jwrite, NULL);
2037 14           register_xs_parse_keyword("jencode", &hooks_jencode, NULL);
2038 14           register_xs_parse_keyword("jpp", &hooks_jpp, NULL);
2039 14           register_xs_parse_keyword("jraw", &hooks_jraw, NULL);
2040              
2041             /* inspection */
2042 14           register_xs_parse_keyword("jtype", &hooks_jtype, NULL);
2043 14           register_xs_parse_keyword("jlen", &hooks_jlen, NULL);
2044 14           register_xs_parse_keyword("jkeys", &hooks_jkeys, NULL);
2045 14           register_xs_parse_keyword("jvals", &hooks_jvals, NULL);
2046 14           register_xs_parse_keyword("jpaths", &hooks_jpaths, NULL);
2047 14           register_xs_parse_keyword("jfind", &hooks_jfind, NULL);
2048              
2049             /* iteration */
2050 14           register_xs_parse_keyword("jiter", &hooks_jiter, NULL);
2051 14           register_xs_parse_keyword("jnext", &hooks_jnext, NULL);
2052 14           register_xs_parse_keyword("jkey", &hooks_jkey, NULL);
2053              
2054             /* patching */
2055 14           register_xs_parse_keyword("jpatch", &hooks_jpatch, NULL);
2056 14           register_xs_parse_keyword("jmerge", &hooks_jmerge, NULL);
2057              
2058             /* comparison */
2059 14           register_xs_parse_keyword("jeq", &hooks_jeq, NULL);
2060              
2061             /* type predicates */
2062 14           register_xs_parse_keyword("jis_obj", &hooks_jis_obj, NULL);
2063 14           register_xs_parse_keyword("jis_arr", &hooks_jis_arr, NULL);
2064 14           register_xs_parse_keyword("jis_str", &hooks_jis_str, NULL);
2065 14           register_xs_parse_keyword("jis_num", &hooks_jis_num, NULL);
2066 14           register_xs_parse_keyword("jis_int", &hooks_jis_int, NULL);
2067 14           register_xs_parse_keyword("jis_real", &hooks_jis_real, NULL);
2068 14           register_xs_parse_keyword("jis_bool", &hooks_jis_bool, NULL);
2069 14           register_xs_parse_keyword("jis_null", &hooks_jis_null, NULL);
2070              
2071             /* alias */
2072 14           register_xs_parse_keyword("jdecode", &hooks_jdecode, NULL);
2073             }
2074              
2075             SV *
2076             new(const char *klass)
2077             CODE:
2078             {
2079             json_yy_t *self;
2080 16           HV *hv = newHV();
2081 16           Newxz(self, 1, json_yy_t);
2082 16           self->flags = F_ALLOW_NONREF;
2083 16           self->max_depth = MAX_DEPTH_DEFAULT;
2084 16           sv_magicext((SV *)hv, NULL, PERL_MAGIC_ext, &json_yy_vtbl,
2085             (const char *)self, 0);
2086 16           RETVAL = sv_bless(newRV_noinc((SV *)hv), gv_stashpv(klass, GV_ADD));
2087             }
2088             OUTPUT:
2089             RETVAL
2090              
2091             void
2092             _set_utf8(SV *self_sv, int val)
2093             CODE:
2094 16 50         if (val) get_self(aTHX_ self_sv)->flags |= F_UTF8;
2095 0           else get_self(aTHX_ self_sv)->flags &= ~F_UTF8;
2096              
2097             void
2098             _set_pretty(SV *self_sv, int val)
2099             CODE:
2100 6 50         if (val) get_self(aTHX_ self_sv)->flags |= F_PRETTY;
2101 0           else get_self(aTHX_ self_sv)->flags &= ~F_PRETTY;
2102              
2103             void
2104             _set_canonical(SV *self_sv, int val)
2105             CODE:
2106 0 0         if (val) get_self(aTHX_ self_sv)->flags |= F_CANONICAL;
2107 0           else get_self(aTHX_ self_sv)->flags &= ~F_CANONICAL;
2108              
2109             void
2110             _set_allow_nonref(SV *self_sv, int val)
2111             CODE:
2112 3 100         if (val) get_self(aTHX_ self_sv)->flags |= F_ALLOW_NONREF;
2113 1           else get_self(aTHX_ self_sv)->flags &= ~F_ALLOW_NONREF;
2114              
2115             void
2116             _set_allow_unknown(SV *self_sv, int val)
2117             CODE:
2118 0 0         if (val) get_self(aTHX_ self_sv)->flags |= F_ALLOW_UNKNOWN;
2119 0           else get_self(aTHX_ self_sv)->flags &= ~F_ALLOW_UNKNOWN;
2120              
2121             void
2122             _set_allow_blessed(SV *self_sv, int val)
2123             CODE:
2124 1 50         if (val) get_self(aTHX_ self_sv)->flags |= F_ALLOW_BLESSED;
2125 0           else get_self(aTHX_ self_sv)->flags &= ~F_ALLOW_BLESSED;
2126              
2127             void
2128             _set_convert_blessed(SV *self_sv, int val)
2129             CODE:
2130 1 50         if (val) get_self(aTHX_ self_sv)->flags |= F_CONVERT_BLESSED;
2131 0           else get_self(aTHX_ self_sv)->flags &= ~F_CONVERT_BLESSED;
2132              
2133             void
2134             _set_max_depth(SV *self_sv, U32 val)
2135             CODE:
2136             {
2137 2           json_yy_t *self = get_self(aTHX_ self_sv);
2138 2           self->max_depth = val;
2139             }
2140              
2141             SV *
2142             decode(SV *self_sv, SV *json_sv)
2143             CODE:
2144             {
2145 6           json_yy_t *self = get_self(aTHX_ self_sv);
2146             STRLEN len;
2147             const char *json;
2148              
2149 6 50         if (self->flags & F_UTF8) {
2150 6           json = SvPV(json_sv, len); /* utf8 mode: input is raw bytes */
2151             } else {
2152 0           json = SvPVutf8(json_sv, len); /* character mode: encode to UTF-8 */
2153             }
2154              
2155             yyjson_read_err err;
2156 6           yyjson_doc *doc = yyjson_read_opts((char *)json, len, YYJSON_READ_NOFLAG, NULL, &err);
2157 6 50         if (!doc)
2158 0           croak("JSON decode error: %s at byte offset %zu", err.msg, err.pos);
2159              
2160 6           yyjson_val *root = yyjson_doc_get_root(doc);
2161 6 50         if (!root) {
2162             yyjson_doc_free(doc);
2163 0           croak("JSON decode error: empty document");
2164             }
2165              
2166             /* check nonref */
2167 6 100         if (!(self->flags & F_ALLOW_NONREF)) {
2168 1           yyjson_type t = yyjson_get_type(root);
2169 1 50         if (t != YYJSON_TYPE_ARR && t != YYJSON_TYPE_OBJ) {
    50          
2170             yyjson_doc_free(doc);
2171 1           croak("JSON text must be an object or array (but found number, string, true, false or null)");
2172             }
2173             }
2174              
2175 5           RETVAL = yyjson_val_to_sv(aTHX_ root);
2176             yyjson_doc_free(doc);
2177             }
2178             OUTPUT:
2179             RETVAL
2180              
2181             SV *
2182             decode_doc(SV *self_sv, SV *json_sv)
2183             CODE:
2184             {
2185 1           json_yy_t *self = get_self(aTHX_ self_sv);
2186             STRLEN len;
2187             const char *json;
2188              
2189 1 50         if (self->flags & F_UTF8) {
2190 1           json = SvPV(json_sv, len);
2191             } else {
2192 0           json = SvPVutf8(json_sv, len);
2193             }
2194              
2195             yyjson_read_err err;
2196 1           yyjson_doc *idoc = yyjson_read_opts((char *)json, len, YYJSON_READ_NOFLAG, NULL, &err);
2197 1 50         if (!idoc)
2198 0           croak("JSON decode error: %s at byte offset %zu", err.msg, err.pos);
2199              
2200 1           yyjson_mut_doc *mdoc = yyjson_doc_mut_copy(idoc, NULL);
2201             yyjson_doc_free(idoc);
2202 1 50         if (!mdoc)
2203 0           croak("decode_doc: failed to create mutable document");
2204              
2205 1           yyjson_mut_val *root = yyjson_mut_doc_get_root(mdoc);
2206 1           RETVAL = new_doc_sv(aTHX_ mdoc, root, NULL);
2207             }
2208             OUTPUT:
2209             RETVAL
2210              
2211             SV *
2212             encode(SV *self_sv, SV *data)
2213             CODE:
2214             {
2215 17           json_yy_t *self = get_self(aTHX_ self_sv);
2216              
2217             /* check nonref */
2218 17 100         if (!(self->flags & F_ALLOW_NONREF)) {
2219 1 50         if (!SvROK(data) || (SvTYPE(SvRV(data)) != SVt_PVAV && SvTYPE(SvRV(data)) != SVt_PVHV))
    0          
    0          
2220 1           croak("hash- or arrayref expected (not a simple scalar)");
2221             }
2222              
2223             /* hybrid: use direct encoder when no yyjson-specific features needed.
2224             note: F_CANONICAL is accepted but not yet implemented (yyjson has no sort-keys).
2225             canonical mode falls through to yyjson path which also doesn't sort,
2226             so at least the output is consistent. */
2227 16 100         if (!(self->flags & F_PRETTY)) {
2228 9           RETVAL = newSV(64);
2229 9           SvPOK_on(RETVAL);
2230 9           SvCUR_set(RETVAL, 0);
2231 9           SAVEFREESV(RETVAL);
2232 9           direct_encode_sv(aTHX_ RETVAL, data, 0, self);
2233 7           SvREFCNT_inc_simple_void_NN(RETVAL);
2234 7           *(SvPVX(RETVAL) + SvCUR(RETVAL)) = '\0';
2235 7 50         if (!(self->flags & F_UTF8))
2236 0           SvUTF8_on(RETVAL);
2237             } else {
2238             /* yyjson path for pretty */
2239 7           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
2240 7           SV *doc_guard = sv_2mortal(newSV(0));
2241 7           sv_magicext(doc_guard, NULL, PERL_MAGIC_ext, &mut_docholder_vtbl,
2242             (const char *)doc, 0);
2243 7           yyjson_mut_val *root = sv_to_yyjson_val(aTHX_ doc, data, self, 0);
2244             yyjson_mut_doc_set_root(doc, root);
2245              
2246             size_t json_len;
2247             yyjson_write_err werr;
2248 5           char *json = yyjson_mut_write_opts(doc, YYJSON_WRITE_PRETTY, NULL, &json_len, &werr);
2249             /* disarm guard before explicit free */
2250 5           mg_findext(doc_guard, PERL_MAGIC_ext, &mut_docholder_vtbl)->mg_ptr = NULL;
2251 5           yyjson_mut_doc_free(doc);
2252              
2253 5 50         if (!json)
2254 0           croak("JSON encode error: %s", werr.msg);
2255              
2256 5 50         if (self->flags & F_UTF8) {
2257 5           RETVAL = newSVpvn(json, json_len);
2258             } else {
2259 0           RETVAL = newSVpvn_utf8(json, json_len, 1);
2260             }
2261 5           free(json);
2262             }
2263             }
2264             OUTPUT:
2265             RETVAL
2266              
2267             SV *
2268             _xs_encode_json(SV *data)
2269             CODE:
2270             {
2271 0           RETVAL = newSV(64);
2272 0           SvPOK_on(RETVAL);
2273 0           SvCUR_set(RETVAL, 0);
2274 0           SAVEFREESV(RETVAL);
2275 0           direct_encode_sv(aTHX_ RETVAL, data, 0, &default_self);
2276 0           SvREFCNT_inc_simple_void_NN(RETVAL);
2277 0           *(SvPVX(RETVAL) + SvCUR(RETVAL)) = '\0';
2278             }
2279             OUTPUT:
2280             RETVAL
2281              
2282             SV *
2283             _xs_decode_json(SV *json_sv)
2284             CODE:
2285             {
2286             STRLEN len;
2287 0           const char *json = SvPV(json_sv, len);
2288              
2289             yyjson_read_err err;
2290 0           yyjson_doc *doc = yyjson_read_opts((char *)json, len, YYJSON_READ_NOFLAG, NULL, &err);
2291 0 0         if (!doc)
2292 0           croak("JSON decode error: %s at byte offset %zu", err.msg, err.pos);
2293              
2294 0           yyjson_val *root = yyjson_doc_get_root(doc);
2295 0 0         if (!root) {
2296             yyjson_doc_free(doc);
2297 0           croak("JSON decode error: empty document");
2298             }
2299              
2300 0           RETVAL = yyjson_val_to_sv(aTHX_ root);
2301             yyjson_doc_free(doc);
2302             }
2303             OUTPUT:
2304             RETVAL
2305              
2306             SV *
2307             _xs_decode_json_ro(SV *json_sv)
2308             CODE:
2309             {
2310             STRLEN len;
2311 0           const char *json = SvPV(json_sv, len);
2312              
2313             yyjson_read_err err;
2314 0           yyjson_doc *doc = yyjson_read_opts((char *)json, len, YYJSON_READ_NOFLAG, NULL, &err);
2315 0 0         if (!doc)
2316 0           croak("JSON decode error: %s at byte offset %zu", err.msg, err.pos);
2317              
2318 0           yyjson_val *root = yyjson_doc_get_root(doc);
2319 0 0         if (!root) {
2320             yyjson_doc_free(doc);
2321 0           croak("JSON decode error: empty document");
2322             }
2323              
2324             /* doc ownership transfers to the holder SV */
2325 0           SV *doc_sv = new_doc_holder(aTHX_ doc);
2326              
2327 0           RETVAL = yyjson_val_to_sv_ro(aTHX_ root, doc_sv);
2328              
2329             /* attach doc_sv to keep yyjson_doc alive while zero-copy SVs exist.
2330             skip for null/bool -- they return immortal globals. */
2331             {
2332 0           yyjson_type rtype = yyjson_get_type(root);
2333 0 0         if (rtype != YYJSON_TYPE_NULL && rtype != YYJSON_TYPE_BOOL) {
    0          
2334 0 0         SV *anchor = SvROK(RETVAL) ? SvRV(RETVAL) : RETVAL;
2335 0           sv_magicext(anchor, doc_sv, PERL_MAGIC_ext, &empty_vtbl, NULL, 0);
2336             }
2337             }
2338 0           SvREFCNT_dec(doc_sv);
2339             }
2340             OUTPUT:
2341             RETVAL
2342              
2343              
2344             # XS helpers for Doc overloading
2345              
2346             SV *
2347             _doc_stringify(SV *self_sv)
2348             CODE:
2349             {
2350 2           json_yy_doc_t *d = get_doc(aTHX_ self_sv);
2351             size_t json_len;
2352             yyjson_write_err werr;
2353 2           char *json = yyjson_mut_val_write_opts(d->root, YYJSON_WRITE_NOFLAG, NULL, &json_len, &werr);
2354 2 50         if (!json)
2355 0           croak("JSON::YY::Doc: stringify error: %s", werr.msg);
2356 2           RETVAL = newSVpvn(json, json_len);
2357 2           free(json);
2358             }
2359             OUTPUT:
2360             RETVAL
2361              
2362             SV *
2363             _doc_eq(SV *a_sv, SV *b_sv)
2364             CODE:
2365             {
2366 2 50         if (!SvROK(b_sv) || !sv_derived_from(b_sv, "JSON::YY::Doc"))
    50          
2367 0           XSRETURN_NO;
2368 2           json_yy_doc_t *a = get_doc(aTHX_ a_sv);
2369 2           json_yy_doc_t *b = get_doc(aTHX_ b_sv);
2370 2 50         RETVAL = yyjson_mut_equals(a->root, b->root)
2371 2 100         ? &PL_sv_yes : &PL_sv_no;
2372 2           SvREFCNT_inc_simple_void_NN(RETVAL);
2373             }
2374             OUTPUT:
2375             RETVAL