File Coverage

YY.xs
Criterion Covered Total %
statement 1168 1324 88.2
branch 564 908 62.1
condition n/a
subroutine n/a
pod n/a
total 1732 2232 77.6


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