File Coverage

YY.xs
Criterion Covered Total %
statement 1125 1325 84.9
branch 577 922 62.5
condition n/a
subroutine n/a
pod n/a
total 1702 2247 75.7


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