File Coverage

Parser.xs
Criterion Covered Total %
statement 304 886 34.3
branch 120 608 19.7
condition n/a
subroutine n/a
pod n/a
total 424 1494 28.3


line stmt bran cond sub pod time code
1             /* DMS::XS::Parser — XS wrapper around the C DMS parser.
2             *
3             * Translates dms_value trees into Perl structures that are byte-compatible
4             * with DMS::Parser (pure Perl): Tie::IxHash tables, blessed DMS::*
5             * sentinels for scalar types, plain scalars for strings.
6             *
7             * Tier-0 only. Surfaces the C parser's attached-comment AST as a
8             * `comments` arrayref on the returned document, mirroring the pure-Perl
9             * parser's `{ meta, body, comments }` shape.
10             */
11             #define PERL_NO_GET_CONTEXT
12             #include "EXTERN.h"
13             #include "perl.h"
14             #include "XSUB.h"
15             #include "ppport.h"
16            
17             #include "dms.h"
18             #include
19             #include
20             #include
21             #include
22            
23             #if defined(WIN32) || defined(_WIN32)
24             # include
25             #else
26             # include
27             #endif
28            
29             /* --- Tie::IxHash table construction ----------------------------------- */
30            
31             /* Cached stash pointers. Filled on first use. */
32             static HV *stash_Bool, *stash_Integer, *stash_Float;
33             static HV *stash_LocalDate, *stash_LocalTime, *stash_LocalDateTime, *stash_OffsetDateTime;
34             static HV *stash_Index;
35            
36 76           static HV *get_stash_cached(pTHX_ HV **slot, const char *name) {
37 76 100         if (!*slot) *slot = gv_stashpv(name, GV_ADD);
38 76           return *slot;
39             }
40            
41             /* Build a blessed scalar-ref sentinel: bless \$inner, .
42             * One alloc (the RV) on top of the inner SV the caller provides — the
43             * earlier blessed-hash shape cost three (HV + HV entry + RV) and showed
44             * up as the Perl-XS marshaling tax on wide-flat integer docs.
45             * The Perl-side classes (DMS::Integer, DMS::Float, DMS::Bool,
46             * DMS::LocalDate, etc.) are now defined against scalar refs accordingly. */
47 76           static SV *bless_sentinel(pTHX_ HV *stash, SV *inner) {
48 76           return sv_bless(newRV_noinc(inner), stash);
49             }
50            
51             /* Cached Tie::IxHash stash. Filled on first use. */
52             static HV *stash_IxHash;
53            
54             /* ---- Fast IxHash construction ----
55             * A Tie::IxHash tied object is a blessed arrayref with the documented
56             * layout:
57             * [ HV{key => index}, AV[keys], AV[values], IV iter ]
58             *
59             * We build that structure directly in C, bypassing Tie::IxHash::TIEHASH /
60             * STORE / FETCH method dispatch. Appending a k/v pair becomes three cheap
61             * C ops (hv_store + av_push + av_push) instead of a full Perl method call
62             * per key. This is the single biggest speedup in the XS port — for tables
63             * with N keys, it collapses N Perl-VM trips into zero.
64             *
65             * Caller gets back the wrapper hashref plus raw pointers to the internal
66             * AV/HV so they can append without re-dereferencing through mg_find on
67             * every insert. Iteration works through the normal tied interface because
68             * the tied object we built is a proper Tie::IxHash instance. */
69 50           static SV *new_ixhash_fast(pTHX_ HV **out_idx, AV **out_keys, AV **out_vals) {
70 50           HV *idx_hv = newHV();
71 50           AV *keys_av = newAV();
72 50           AV *vals_av = newAV();
73            
74 50           AV *ix_obj = newAV();
75 50           av_extend(ix_obj, 3);
76 50           av_store(ix_obj, 0, newRV_noinc((SV *)idx_hv));
77 50           av_store(ix_obj, 1, newRV_noinc((SV *)keys_av));
78 50           av_store(ix_obj, 2, newRV_noinc((SV *)vals_av));
79 50           av_store(ix_obj, 3, newSViv(0));
80            
81 50 100         if (!stash_IxHash) {
82             /* Lazy-load Tie::IxHash on first full-mode parse — Parser.pm
83             * no longer `use`s it at compile time, so lite-mode-only callers
84             * (bench drivers) don't pay the ~7 ms .pm load. The require
85             * defines Tie::IxHash::FIRSTKEY/NEXTKEY/FETCH/etc., which Perl
86             * looks up lazily when user code does `keys %$tied`. */
87 2           load_module(PERL_LOADMOD_NOIMPORT, newSVpvs("Tie::IxHash"), NULL);
88 2           stash_IxHash = gv_stashpv("Tie::IxHash", GV_ADD);
89             }
90 50           SV *tied_rv = newRV_noinc((SV *)ix_obj);
91 50           sv_bless(tied_rv, stash_IxHash);
92            
93 50           HV *wrapper = newHV();
94 50           sv_magic((SV *)wrapper, tied_rv, PERL_MAGIC_tied, NULL, 0);
95 50           SvREFCNT_dec(tied_rv);
96            
97 50 50         if (out_idx) *out_idx = idx_hv;
98 50 50         if (out_keys) *out_keys = keys_av;
99 50 50         if (out_vals) *out_vals = vals_av;
100            
101 50           return newRV_noinc((SV *)wrapper);
102             }
103            
104             /* Append a k/v pair to an IxHash built via new_ixhash_fast. Caller owns
105             the val refcount; we transfer it into the vals AV. Key is UTF-8 bytes;
106             we flag the resulting key SV as UTF-8 for lookup correctness on
107             non-ASCII keys. */
108 92           static void ixhash_append(pTHX_ HV *idx, AV *keys, AV *vals,
109             const char *key, STRLEN klen, SV *val) {
110 92           SSize_t pos = av_len(keys) + 1;
111            
112 92           SV *key_sv = newSVpvn(key, klen);
113 92           sv_utf8_decode(key_sv);
114 92           hv_store_ent(idx, key_sv, newSViv((IV)pos), 0);
115 92           av_push(keys, key_sv);
116 92           av_push(vals, val);
117 92           }
118            
119             /* --- dms_value → SV -------------------------------------------------- *
120             *
121             * Two construction shapes:
122             * - Full mode (value_to_sv): tables are Tie::IxHash (preserves key
123             * order under the standard `keys %$h` interface). Required for the
124             * full-mode round-trip path because comments + original_forms get
125             * attached by path-key, and re-walking the document on emit needs
126             * stable insertion order.
127             *
128             * - Lite mode (value_to_sv_lite): tables are plain HVs with a sidecar
129             * `__dms_keys` arrayref stored at key "\0__dms_keys" (NUL prefix
130             * can never collide with a real DMS key). Iterators that care
131             * about order read the sidecar; iterators that don't can use plain
132             * `keys %$h` (with the implicit understanding that they'll see
133             * the sidecar key).
134             *
135             * Skipping Tie::IxHash setup saves ~6 SV allocations per table — on
136             * bench_realistic that's the dominant residual gap vs YAML::XS. */
137            
138             static SV *value_to_sv_lite(pTHX_ const dms_value *v);
139            
140 146           static SV *value_to_sv(pTHX_ const dms_value *v) {
141 146           switch (v->type) {
142 0           case DMS_BOOL: {
143 0           return bless_sentinel(aTHX_
144             get_stash_cached(aTHX_ &stash_Bool, "DMS::Bool"),
145 0           newSViv(v->u.b ? 1 : 0));
146             }
147 75           case DMS_INTEGER: {
148             /* Store as a native IV — 64-bit on 64-bit Perl. bstr() on the
149             * Perl side stringifies on demand. Skips snprintf + newSVpv. */
150 75           return bless_sentinel(aTHX_
151             get_stash_cached(aTHX_ &stash_Integer, "DMS::Integer"),
152 75           newSViv((IV)v->u.i));
153             }
154 0           case DMS_FLOAT: {
155 0           return bless_sentinel(aTHX_
156             get_stash_cached(aTHX_ &stash_Float, "DMS::Float"),
157 0           newSVnv(v->u.f));
158             }
159 18           case DMS_STRING: {
160 18 50         SV *sv = newSVpv(v->u.s ? v->u.s : "", 0);
161 18           sv_utf8_decode(sv);
162 18           return sv;
163             }
164 0           case DMS_OFFSET_DT:
165             case DMS_LOCAL_DT:
166             case DMS_LOCAL_DATE:
167             case DMS_LOCAL_TIME: {
168             HV **slot;
169             const char *name;
170 0 0         if (v->type == DMS_OFFSET_DT) { slot = &stash_OffsetDateTime; name = "DMS::OffsetDateTime"; }
171 0 0         else if (v->type == DMS_LOCAL_DT) { slot = &stash_LocalDateTime; name = "DMS::LocalDateTime"; }
172 0 0         else if (v->type == DMS_LOCAL_DATE) { slot = &stash_LocalDate; name = "DMS::LocalDate"; }
173 0           else { slot = &stash_LocalTime; name = "DMS::LocalTime"; }
174 0 0         SV *inner = newSVpv(v->u.s ? v->u.s : "", 0);
175 0           sv_utf8_decode(inner);
176 0           return bless_sentinel(aTHX_ get_stash_cached(aTHX_ slot, name), inner);
177             }
178 50           case DMS_TABLE: {
179             HV *idx_hv; AV *keys_av; AV *vals_av;
180 50           SV *href = new_ixhash_fast(aTHX_ &idx_hv, &keys_av, &vals_av);
181             /* Pre-size the AVs so av_push doesn't reallocate during build. */
182 50 50         if (v->u.t.len > 0) {
183 50           av_extend(keys_av, (SSize_t)v->u.t.len - 1);
184 50           av_extend(vals_av, (SSize_t)v->u.t.len - 1);
185             }
186 142 100         for (size_t i = 0; i < v->u.t.len; i++) {
187 92           SV *val_sv = value_to_sv(aTHX_ v->u.t.items[i].value);
188 92           ixhash_append(aTHX_ idx_hv, keys_av, vals_av,
189 92           v->u.t.items[i].key,
190 92           strlen(v->u.t.items[i].key),
191             val_sv);
192             }
193 50           return href;
194             }
195 3           case DMS_LIST: {
196 3           AV *av = newAV();
197 3           av_extend(av, (SSize_t)v->u.l.len);
198 11 100         for (size_t i = 0; i < v->u.l.len; i++) {
199 8           av_push(av, value_to_sv(aTHX_ v->u.l.items[i]));
200             }
201 3           return newRV_noinc((SV *)av);
202             }
203             }
204 0           return &PL_sv_undef;
205             }
206            
207             /* Lite-mode table construction: plain HV + sidecar __dms_keys AV.
208             * Skips Tie::IxHash setup (saves ~6 SVs per table). Sidecar key is
209             * "\0__dms_keys" — the NUL prefix guarantees no collision with any
210             * real DMS key, and `keys %$h` iteration order doesn't matter in lite
211             * mode (consumers read the sidecar AV for order). */
212             static const char SIDECAR_KEY[] = "\0__dms_keys";
213             #define SIDECAR_KEY_LEN 11
214            
215 12           static SV *value_to_sv_lite(pTHX_ const dms_value *v) {
216 12           switch (v->type) {
217 0           case DMS_BOOL: {
218 0           return bless_sentinel(aTHX_
219             get_stash_cached(aTHX_ &stash_Bool, "DMS::Bool"),
220 0           newSViv(v->u.b ? 1 : 0));
221             }
222 0           case DMS_INTEGER: {
223 0           return bless_sentinel(aTHX_
224             get_stash_cached(aTHX_ &stash_Integer, "DMS::Integer"),
225 0           newSViv((IV)v->u.i));
226             }
227 0           case DMS_FLOAT: {
228 0           return bless_sentinel(aTHX_
229             get_stash_cached(aTHX_ &stash_Float, "DMS::Float"),
230 0           newSVnv(v->u.f));
231             }
232 4           case DMS_STRING: {
233 4 50         SV *sv = newSVpv(v->u.s ? v->u.s : "", 0);
234 4           sv_utf8_decode(sv);
235 4           return sv;
236             }
237 0           case DMS_OFFSET_DT:
238             case DMS_LOCAL_DT:
239             case DMS_LOCAL_DATE:
240             case DMS_LOCAL_TIME: {
241             HV **slot;
242             const char *name;
243 0 0         if (v->type == DMS_OFFSET_DT) { slot = &stash_OffsetDateTime; name = "DMS::OffsetDateTime"; }
244 0 0         else if (v->type == DMS_LOCAL_DT) { slot = &stash_LocalDateTime; name = "DMS::LocalDateTime"; }
245 0 0         else if (v->type == DMS_LOCAL_DATE) { slot = &stash_LocalDate; name = "DMS::LocalDate"; }
246 0           else { slot = &stash_LocalTime; name = "DMS::LocalTime"; }
247 0 0         SV *inner = newSVpv(v->u.s ? v->u.s : "", 0);
248 0           sv_utf8_decode(inner);
249 0           return bless_sentinel(aTHX_ get_stash_cached(aTHX_ slot, name), inner);
250             }
251 8           case DMS_TABLE: {
252 8           HV *hv = newHV();
253 8           AV *keys_av = newAV();
254 8 100         if (v->u.t.len > 0) {
255 3           av_extend(keys_av, (SSize_t)v->u.t.len - 1);
256 3           hv_ksplit(hv, (U32)(v->u.t.len + 1));
257             }
258 12 100         for (size_t i = 0; i < v->u.t.len; i++) {
259 4           const char *key = v->u.t.items[i].key;
260 4           STRLEN klen = strlen(key);
261 4           SV *val_sv = value_to_sv_lite(aTHX_ v->u.t.items[i].value);
262             /* Store value into HV. hv_store consumes the value SV's
263             * refcount (one). */
264 4           hv_store(hv, key, (I32)klen, val_sv, 0);
265             /* Append a UTF-8-flagged key SV to the sidecar. */
266 4           SV *key_sv = newSVpvn(key, klen);
267 4           sv_utf8_decode(key_sv);
268 4           av_push(keys_av, key_sv);
269             }
270             /* Sidecar at "\0__dms_keys" — NUL prefix avoids collision. */
271 8           hv_store(hv, SIDECAR_KEY, (I32)SIDECAR_KEY_LEN,
272             newRV_noinc((SV *)keys_av), 0);
273 8           return newRV_noinc((SV *)hv);
274             }
275 0           case DMS_LIST: {
276 0           AV *av = newAV();
277 0           av_extend(av, (SSize_t)v->u.l.len);
278 0 0         for (size_t i = 0; i < v->u.l.len; i++) {
279 0           av_push(av, value_to_sv_lite(aTHX_ v->u.l.items[i]));
280             }
281 0           return newRV_noinc((SV *)av);
282             }
283             }
284 0           return &PL_sv_undef;
285             }
286            
287             /* --- comment AST → SV -------------------------------------------------- */
288            
289             /* Build the comment hashref { content, kind } mirroring the pure-Perl
290             * parser. `content` is the raw source text (UTF-8) including delimiters.
291             * `kind` is "line" or "block". */
292 41           static SV *comment_to_sv(pTHX_ const dms_attached_comment *ac) {
293 41           HV *h = newHV();
294 41 50         SV *content_sv = newSVpv(ac->content ? ac->content : "", 0);
295 41           sv_utf8_decode(content_sv);
296 41           hv_store(h, "content", 7, content_sv, 0);
297 41 100         const char *kind = (ac->kind == DMS_COMMENT_BLOCK) ? "block" : "line";
298 41           hv_store(h, "kind", 4, newSVpv(kind, 0), 0);
299 41           return newRV_noinc((SV *)h);
300             }
301            
302             /* Build a path arrayref from a dms_breadcrumb_seg array. String segments
303             * are plain Perl scalars (UTF-8 decoded); index segments are blessed
304             * DMS::Index scalar refs (matching the pure-Perl parser's wrapper).
305             * Used by both attached-comment paths and original-form-entry paths. */
306 62           static SV *path_segs_to_sv(pTHX_ const dms_breadcrumb_seg *segs, size_t n) {
307 62           AV *av = newAV();
308 62 100         if (n > 0) av_extend(av, (SSize_t)n - 1);
309 124 100         for (size_t i = 0; i < n; i++) {
310 62           const dms_breadcrumb_seg *seg = &segs[i];
311 62 100         if (seg->is_index) {
312 1           HV *st = get_stash_cached(aTHX_ &stash_Index, "DMS::Index");
313 1           av_push(av, bless_sentinel(aTHX_ st, newSViv((IV)seg->idx)));
314             } else {
315 61 50         SV *k = newSVpv(seg->key ? seg->key : "", 0);
316 61           sv_utf8_decode(k);
317 61           av_push(av, k);
318             }
319             }
320 62           return newRV_noinc((SV *)av);
321             }
322            
323 41           static SV *path_to_sv(pTHX_ const dms_attached_comment *ac) {
324 41           return path_segs_to_sv(aTHX_ ac->path, ac->path_len);
325             }
326            
327             /* Build the `string_form` hashref for an original-literal record whose
328             * lit.is_string_form == 1. Mirrors the shape DMS::Emitter expects:
329             * { kind => 'basic'|'literal'|'heredoc',
330             * flavor => 'basic_triple'|'literal_triple' (heredoc only),
331             * label => "...", (heredoc only)
332             * modifiers => [ { name => "...", args => [...] }, ... ] (heredoc only) }
333             * The `args` array is left empty for now — the C struct stores
334             * `dms_value **args`, but heredoc modifier args round-trip through the
335             * lexeme buffer in dms-c, and the Perl Emitter currently only inspects
336             * `name` (it re-applies the modifier via dispatch on name). */
337 6           static SV *string_form_to_sv(pTHX_ const dms_string_form *sf) {
338 6           HV *h = newHV();
339 6           const char *kind =
340 12 50         (sf->kind == DMS_STRING_BASIC) ? "basic" :
341 6 100         (sf->kind == DMS_STRING_LITERAL) ? "literal" :
342             "heredoc";
343 6           hv_store(h, "kind", 4, newSVpv(kind, 0), 0);
344 6 100         if (sf->kind == DMS_STRING_HEREDOC) {
345 6           const char *flavor = (sf->heredoc_flavor == DMS_HEREDOC_BASIC_TRIPLE)
346 3 50         ? "basic_triple" : "literal_triple";
347 3           hv_store(h, "flavor", 6, newSVpv(flavor, 0), 0);
348 3 50         if (sf->label) {
349 3           SV *lbl = newSVpv(sf->label, 0);
350 3           sv_utf8_decode(lbl);
351 3           hv_store(h, "label", 5, lbl, 0);
352             } else {
353 0           hv_store(h, "label", 5, newSV(0), 0);
354             }
355 3           AV *mods = newAV();
356 3 50         if (sf->num_modifiers > 0) av_extend(mods, (SSize_t)sf->num_modifiers - 1);
357 3 50         for (size_t i = 0; i < sf->num_modifiers; i++) {
358 0           HV *m = newHV();
359 0           const dms_heredoc_modifier_call *mc = &sf->modifiers[i];
360 0 0         SV *name = newSVpv(mc->name ? mc->name : "", 0);
361 0           sv_utf8_decode(name);
362 0           hv_store(m, "name", 4, name, 0);
363             /* args: marshal the dms_value array to a Perl arrayref via the
364             * existing value_to_sv() so heredoc modifier args (e.g. "\n",
365             * ">") survive the round-trip. */
366 0           AV *args = newAV();
367 0 0         if (mc->num_args > 0) av_extend(args, (SSize_t)mc->num_args - 1);
368 0 0         for (size_t j = 0; j < mc->num_args; j++) {
369 0 0         if (mc->args[j]) {
370 0           av_push(args, value_to_sv(aTHX_ mc->args[j]));
371             } else {
372 0           av_push(args, newSV(0));
373             }
374             }
375 0           hv_store(m, "args", 4, newRV_noinc((SV *)args), 0);
376 0           av_push(mods, newRV_noinc((SV *)m));
377             }
378 3           hv_store(h, "modifiers", 9, newRV_noinc((SV *)mods), 0);
379             }
380 6           return newRV_noinc((SV *)h);
381             }
382            
383             /* Build the per-entry lit hashref. The Emitter checks for the presence
384             * of `integer_lit` vs `string_form` keys to dispatch — exactly one is
385             * populated per entry, matching the C struct's `is_string_form` flag. */
386 21           static SV *original_lit_to_sv(pTHX_ const dms_original_literal *lit) {
387 21           HV *h = newHV();
388 21 100         if (lit->is_string_form) {
389 6 50         if (lit->string_form) {
390 6           hv_store(h, "string_form", 11, string_form_to_sv(aTHX_ lit->string_form), 0);
391             }
392             } else {
393 15 50         if (lit->integer_lit) {
394 15           SV *s = newSVpv(lit->integer_lit, 0);
395             /* integer_lit is ASCII (digits + 0x/0o/0b prefixes + underscores)
396             * — no UTF-8 decode needed. */
397 15           hv_store(h, "integer_lit", 11, s, 0);
398             }
399             }
400 21           return newRV_noinc((SV *)h);
401             }
402            
403             /* Convert the C original-forms array to the Perl `[[path, lit], ...]`
404             * shape DMS::Emitter expects. Returns an empty arrayref (not undef) when
405             * `n == 0` so the Emitter's `|| []` guard is the only fallback path. */
406 40           static SV *original_forms_to_sv(pTHX_ const dms_original_form_entry *items, size_t n) {
407 40           AV *av = newAV();
408 40 100         if (n > 0) av_extend(av, (SSize_t)n - 1);
409 61 100         for (size_t i = 0; i < n; i++) {
410 21           const dms_original_form_entry *e = &items[i];
411 21           AV *pair = newAV();
412 21           av_extend(pair, 1);
413 21           av_push(pair, path_segs_to_sv(aTHX_ e->path, e->path_len));
414 21           av_push(pair, original_lit_to_sv(aTHX_ &e->lit));
415 21           av_push(av, newRV_noinc((SV *)pair));
416             }
417 40           return newRV_noinc((SV *)av);
418             }
419            
420 40           static SV *comments_to_sv(pTHX_ const dms_attached_comment *items, size_t n) {
421 40           AV *av = newAV();
422 40 100         if (n > 0) av_extend(av, (SSize_t)n - 1);
423 81 100         for (size_t i = 0; i < n; i++) {
424 41           const dms_attached_comment *ac = &items[i];
425 41           HV *h = newHV();
426 41           hv_store(h, "comment", 7, comment_to_sv(aTHX_ ac), 0);
427 41           const char *pos =
428 62 100         (ac->position == DMS_COMMENT_LEADING) ? "leading" :
429 21 50         (ac->position == DMS_COMMENT_INNER) ? "inner" :
430 21 100         (ac->position == DMS_COMMENT_TRAILING) ? "trailing" :
431             "floating";
432 41           hv_store(h, "position", 8, newSVpv(pos, 0), 0);
433 41           hv_store(h, "path", 4, path_to_sv(aTHX_ ac), 0);
434 41           av_push(av, newRV_noinc((SV *)h));
435             }
436 40           return newRV_noinc((SV *)av);
437             }
438            
439             /* --- Direct DMS -> conformance JSON streaming emit -------------------------
440             *
441             * For workloads where the only consumer of the parse tree is a JSON-emit
442             * step (e.g. the conformance encoder, dms-tests harness), building the
443             * full Perl SV/HV/AV/Tie::IxHash tree just to walk it once is pure waste.
444             * `parse_to_json_bytes(src)` skips that round trip: it parses, then
445             * serializes the dms_value tree directly into a single Perl string buffer
446             * in C. No blessed sentinels, no IxHash tied magic, no per-leaf Perl call
447             * frame.
448             *
449             * Output shape matches encoder.pl's `encode_json_value` byte-for-byte at
450             * the structural level (the conformance runner re-parses the JSON, so
451             * exact whitespace doesn't matter, only key order in objects). Tagged
452             * scalars look like { "type": "...", "value": "..." }; tables are objects
453             * preserving source key order; lists are arrays. Front matter, when
454             * present, is wrapped as { "_meta": ..., "_body": ... } per the spec. */
455            
456             typedef struct {
457             char *buf;
458             size_t len;
459             size_t cap;
460             } jbuf;
461            
462             /* XSUB.h #defines `realloc`/`free` as Perl's PerlMem_* macros which
463             * require a thread-context argument. Our jbuf doesn't need that
464             * indirection — it's a leaf C buffer with no Perl interaction — so we
465             * #undef the macros and route through small libc-direct wrappers. The
466             * Perl-side allocator is irrelevant here: jbuf memory lives only across
467             * one XS call and is freed before returning. */
468             #ifdef realloc
469             # undef realloc
470             #endif
471             #ifdef free
472             # undef free
473             #endif
474            
475 0           static void *libc_realloc(void *p, size_t n) { return realloc(p, n); }
476 0           static void libc_free(void *p) { free(p); }
477            
478 0           static void jbuf_grow(jbuf *j, size_t need) {
479 0           size_t want = j->len + need;
480 0 0         if (want <= j->cap) return;
481 0 0         size_t cap = j->cap ? j->cap : 4096;
482 0 0         while (cap < want) cap *= 2;
483 0           j->buf = (char *)libc_realloc(j->buf, cap);
484 0           j->cap = cap;
485             }
486            
487 0           static inline void jbuf_putc(jbuf *j, char c) {
488 0 0         if (j->len + 1 > j->cap) jbuf_grow(j, 1);
489 0           j->buf[j->len++] = c;
490 0           }
491            
492 0           static inline void jbuf_puts(jbuf *j, const char *s, size_t n) {
493 0 0         if (j->len + n > j->cap) jbuf_grow(j, n);
494 0           memcpy(j->buf + j->len, s, n);
495 0           j->len += n;
496 0           }
497            
498 0           static inline void jbuf_putcstr(jbuf *j, const char *s) {
499 0           jbuf_puts(j, s, strlen(s));
500 0           }
501            
502             /* Indent: 2 spaces per level. */
503 0           static void jbuf_indent(jbuf *j, int n) {
504 0 0         if (n <= 0) return;
505 0           size_t need = (size_t)n * 2;
506 0 0         if (j->len + need > j->cap) jbuf_grow(j, need);
507 0           memset(j->buf + j->len, ' ', need);
508 0           j->len += need;
509             }
510            
511             /* JSON-quote a UTF-8 string. We escape only the JSON-mandated control
512             * characters and quote/backslash; bytes >= 0x20 are passed through
513             * verbatim (they're already valid UTF-8 from the parser). */
514 0           static void jbuf_quote(jbuf *j, const char *s, size_t n) {
515 0           jbuf_putc(j, '"');
516 0 0         for (size_t i = 0; i < n; i++) {
517 0           unsigned char c = (unsigned char)s[i];
518 0           switch (c) {
519 0           case '"': jbuf_puts(j, "\\\"", 2); break;
520 0           case '\\': jbuf_puts(j, "\\\\", 2); break;
521 0           case '\n': jbuf_puts(j, "\\n", 2); break;
522 0           case '\r': jbuf_puts(j, "\\r", 2); break;
523 0           case '\t': jbuf_puts(j, "\\t", 2); break;
524 0           case '\b': jbuf_puts(j, "\\b", 2); break;
525 0           case '\f': jbuf_puts(j, "\\f", 2); break;
526 0           default:
527 0 0         if (c < 0x20) {
528             char tmp[8];
529 0           int k = snprintf(tmp, sizeof(tmp), "\\u%04x", c);
530 0           jbuf_puts(j, tmp, (size_t)k);
531             } else {
532 0           jbuf_putc(j, (char)c);
533             }
534             }
535             }
536 0           jbuf_putc(j, '"');
537 0           }
538            
539 0           static void jbuf_quote_cstr(jbuf *j, const char *s) {
540 0           jbuf_quote(j, s, strlen(s));
541 0           }
542            
543             /* Render a 64-bit integer. Faster than snprintf for the common case. */
544 0           static void jbuf_int64(jbuf *j, int64_t v) {
545             char tmp[24];
546             int n;
547 0 0         if (v == INT64_MIN) {
548 0           n = snprintf(tmp, sizeof(tmp), "%" PRId64, v);
549             } else {
550 0           int neg = v < 0;
551 0 0         uint64_t u = neg ? (uint64_t)(-v) : (uint64_t)v;
552 0           char *p = tmp + sizeof(tmp);
553 0 0         do { *--p = (char)('0' + (u % 10)); u /= 10; } while (u);
554 0 0         if (neg) *--p = '-';
555 0           n = (int)((tmp + sizeof(tmp)) - p);
556 0           jbuf_puts(j, p, (size_t)n);
557 0           return;
558             }
559 0           jbuf_puts(j, tmp, (size_t)n);
560             }
561            
562             /* Render a double in shortest-round-trippable form, matching the
563             * encoder.pl `shortest_float` rules:
564             * - nan / inf / -inf are emitted as literals
565             * - search %.1g..%.17g for the shortest representation that round-trips
566             * - normalize exponent: "e+12" -> "e12", "e-04" -> "e-4", "e07" -> "e7"
567             * - if the result has no '.' or 'e'/'E', append ".0" */
568 0           static void jbuf_float(jbuf *j, double v) {
569             /* nan / inf */
570 0 0         if (v != v) { jbuf_putcstr(j, "nan"); return; }
571 0 0         if (v > 1.7976931348623157e308) { jbuf_putcstr(j, "inf"); return; }
572 0 0         if (v < -1.7976931348623157e308) { jbuf_putcstr(j, "-inf"); return; }
573            
574             char buf[64];
575 0           int chosen_n = 0;
576 0 0         for (int p = 1; p <= 17; p++) {
577 0           int n = snprintf(buf, sizeof(buf), "%.*g", p, v);
578 0           double back = strtod(buf, NULL);
579 0 0         if (back == v) { chosen_n = n; break; }
580             }
581 0 0         if (chosen_n == 0) {
582 0           chosen_n = snprintf(buf, sizeof(buf), "%.17g", v);
583             }
584            
585             /* Post-process exponent. */
586             char out[64];
587 0           int oi = 0;
588 0           int has_dot_or_e = 0;
589 0 0         for (int i = 0; i < chosen_n; i++) {
590 0           char c = buf[i];
591 0 0         if (c == '.') has_dot_or_e = 1;
592 0 0         if (c == 'e' || c == 'E') {
    0          
593 0           has_dot_or_e = 1;
594 0           out[oi++] = c;
595 0           i++;
596 0           int sign = 1;
597 0 0         if (i < chosen_n && buf[i] == '+') { i++; }
    0          
598 0 0         else if (i < chosen_n && buf[i] == '-') { sign = -1; i++; }
    0          
599             /* skip leading zeros */
600 0 0         while (i < chosen_n && buf[i] == '0') i++;
    0          
601 0 0         if (i >= chosen_n || buf[i] < '0' || buf[i] > '9') {
    0          
    0          
602             /* exponent collapsed to zero — drop it */
603             /* Pop the 'e' we already wrote. */
604 0           oi--;
605 0           continue;
606             }
607 0 0         if (sign < 0) out[oi++] = '-';
608 0 0         while (i < chosen_n) out[oi++] = buf[i++];
609 0           break;
610             }
611 0           out[oi++] = c;
612             }
613 0 0         if (!has_dot_or_e) {
614 0           out[oi++] = '.';
615 0           out[oi++] = '0';
616             }
617 0           jbuf_puts(j, out, (size_t)oi);
618             }
619            
620             static void emit_value(jbuf *j, const dms_value *v, int indent);
621            
622             /* Emit a tagged scalar:
623             * {
624             * "type": "",
625             * "value": ""
626             * }
627             * with the same indentation rules as encoder.pl. */
628 0           static void emit_tagged(jbuf *j, const char *type, int indent,
629             void (*write_value)(jbuf *, const dms_value *),
630             const dms_value *v) {
631 0           jbuf_puts(j, "{\n", 2);
632 0           jbuf_indent(j, indent + 1);
633 0           jbuf_puts(j, "\"type\": \"", 9);
634 0           jbuf_putcstr(j, type);
635 0           jbuf_puts(j, "\",\n", 3);
636 0           jbuf_indent(j, indent + 1);
637 0           jbuf_puts(j, "\"value\": \"", 10);
638 0           write_value(j, v);
639 0           jbuf_puts(j, "\"\n", 2);
640 0           jbuf_indent(j, indent);
641 0           jbuf_putc(j, '}');
642 0           }
643            
644             /* write_value callbacks — write the inner string of a tagged scalar
645             * (no surrounding quotes; the caller already wrote them). */
646 0           static void wv_bool(jbuf *j, const dms_value *v) {
647 0 0         jbuf_putcstr(j, v->u.b ? "true" : "false");
648 0           }
649            
650 0           static void wv_int(jbuf *j, const dms_value *v) {
651 0           jbuf_int64(j, v->u.i);
652 0           }
653            
654 0           static void wv_float(jbuf *j, const dms_value *v) {
655 0           jbuf_float(j, v->u.f);
656 0           }
657            
658             /* Datetime values: stored as a UTF-8 NUL-terminated string in v->u.s.
659             * No JSON escaping is needed (datetime/date/time strings are ASCII), but
660             * for safety we run them through the same escape code as other strings. */
661 0           static void wv_str_escape(jbuf *j, const dms_value *v) {
662 0 0         const char *s = v->u.s ? v->u.s : "";
663 0 0         for (const char *p = s; *p; p++) {
664 0           unsigned char c = (unsigned char)*p;
665 0           switch (c) {
666 0           case '"': jbuf_puts(j, "\\\"", 2); break;
667 0           case '\\': jbuf_puts(j, "\\\\", 2); break;
668 0           case '\n': jbuf_puts(j, "\\n", 2); break;
669 0           case '\r': jbuf_puts(j, "\\r", 2); break;
670 0           case '\t': jbuf_puts(j, "\\t", 2); break;
671 0           case '\b': jbuf_puts(j, "\\b", 2); break;
672 0           case '\f': jbuf_puts(j, "\\f", 2); break;
673 0           default:
674 0 0         if (c < 0x20) {
675             char tmp[8];
676 0           int k = snprintf(tmp, sizeof(tmp), "\\u%04x", c);
677 0           jbuf_puts(j, tmp, (size_t)k);
678             } else {
679 0           jbuf_putc(j, (char)c);
680             }
681             }
682             }
683 0           }
684            
685 0           static void emit_value(jbuf *j, const dms_value *v, int indent) {
686 0           switch (v->type) {
687 0           case DMS_BOOL:
688 0           emit_tagged(j, "bool", indent, wv_bool, v); return;
689 0           case DMS_INTEGER:
690 0           emit_tagged(j, "integer", indent, wv_int, v); return;
691 0           case DMS_FLOAT:
692 0           emit_tagged(j, "float", indent, wv_float, v); return;
693 0           case DMS_STRING:
694 0           emit_tagged(j, "string", indent, wv_str_escape, v); return;
695 0           case DMS_OFFSET_DT:
696 0           emit_tagged(j, "offset-datetime", indent, wv_str_escape, v); return;
697 0           case DMS_LOCAL_DT:
698 0           emit_tagged(j, "local-datetime", indent, wv_str_escape, v); return;
699 0           case DMS_LOCAL_DATE:
700 0           emit_tagged(j, "local-date", indent, wv_str_escape, v); return;
701 0           case DMS_LOCAL_TIME:
702 0           emit_tagged(j, "local-time", indent, wv_str_escape, v); return;
703 0           case DMS_TABLE: {
704 0 0         if (v->u.t.len == 0) { jbuf_puts(j, "{}", 2); return; }
705 0           jbuf_puts(j, "{\n", 2);
706 0 0         for (size_t i = 0; i < v->u.t.len; i++) {
707 0           jbuf_indent(j, indent + 1);
708 0           const char *k = v->u.t.items[i].key;
709 0           jbuf_quote(j, k, strlen(k));
710 0           jbuf_puts(j, ": ", 2);
711 0           emit_value(j, v->u.t.items[i].value, indent + 1);
712 0 0         if (i + 1 < v->u.t.len) jbuf_putc(j, ',');
713 0           jbuf_putc(j, '\n');
714             }
715 0           jbuf_indent(j, indent);
716 0           jbuf_putc(j, '}');
717 0           return;
718             }
719 0           case DMS_LIST: {
720 0 0         if (v->u.l.len == 0) { jbuf_puts(j, "[]", 2); return; }
721 0           jbuf_puts(j, "[\n", 2);
722 0 0         for (size_t i = 0; i < v->u.l.len; i++) {
723 0           jbuf_indent(j, indent + 1);
724 0           emit_value(j, v->u.l.items[i], indent + 1);
725 0 0         if (i + 1 < v->u.l.len) jbuf_putc(j, ',');
726 0           jbuf_putc(j, '\n');
727             }
728 0           jbuf_indent(j, indent);
729 0           jbuf_putc(j, ']');
730 0           return;
731             }
732             }
733             }
734            
735             /* Top-level: wrap with { "_meta": ..., "_body": ... } when meta is
736             * present, otherwise just emit the body. Always trailing '\n'. */
737 0           static void emit_document(jbuf *j, const dms_document *doc) {
738 0 0         if (doc->meta) {
739             dms_value mv;
740 0           mv.type = DMS_TABLE;
741 0           mv.u.t = *doc->meta;
742 0           jbuf_puts(j, "{\n", 2);
743 0           jbuf_indent(j, 1);
744 0           jbuf_puts(j, "\"_meta\": ", 9);
745 0           emit_value(j, &mv, 1);
746 0           jbuf_puts(j, ",\n", 2);
747 0           jbuf_indent(j, 1);
748 0           jbuf_puts(j, "\"_body\": ", 9);
749 0           emit_value(j, doc->body, 1);
750 0           jbuf_puts(j, "\n}", 2);
751             } else {
752 0           emit_value(j, doc->body, 0);
753             }
754 0           jbuf_putc(j, '\n');
755 0           }
756            
757             /* --- C-side to_dms_lite emitter ---------------------------------------- *
758             *
759             * Walks a Perl Document tree (the shape returned by parse_document_lite)
760             * and emits canonical DMS source bytes directly into a Perl SV. Skips
761             * the per-kvpair Perl-VM trips of the pure-Perl emitter — for the
762             * realistic 25 KB fixture this drops emit cost from ~2 ms (post-pure-
763             * Perl-optimisation) to a fraction of that, finally beating YAML::XS's
764             * libyaml-backed Dump.
765             *
766             * Scope: lite mode only. Skips comment-AST + original_forms by
767             * construction (those maps are empty in lite mode anyway). For full-
768             * mode round-trip, the pure-Perl Emitter still owns the path: comment
769             * walking and per-path original-form lookups are easier to express in
770             * Perl and the 25 KB fixture isn't the worst case for full mode. */
771            
772             typedef struct {
773             char *buf;
774             STRLEN len;
775             STRLEN cap;
776             } dbuf;
777            
778 2           static void dbuf_init(pTHX_ dbuf *d, STRLEN cap) {
779 2           Newx(d->buf, cap, char);
780 2           d->len = 0;
781 2           d->cap = cap;
782 2           }
783            
784 2           static void dbuf_free(pTHX_ dbuf *d) {
785 2 50         if (d->buf) Safefree(d->buf);
786 2           d->buf = NULL;
787 2           d->len = d->cap = 0;
788 2           }
789            
790 0           static void dbuf_grow(pTHX_ dbuf *d, STRLEN need) {
791 0           STRLEN newcap = d->cap;
792 0 0         if (newcap < 256) newcap = 256;
793 0 0         while (newcap < d->len + need) newcap *= 2;
794 0           Renew(d->buf, newcap, char);
795 0           d->cap = newcap;
796 0           }
797            
798 12           static inline void dbuf_putc(pTHX_ dbuf *d, char c) {
799 12 50         if (d->len + 1 > d->cap) dbuf_grow(aTHX_ d, 1);
800 12           d->buf[d->len++] = c;
801 12           }
802            
803 8           static inline void dbuf_puts(pTHX_ dbuf *d, const char *s, STRLEN n) {
804 8 50         if (d->len + n > d->cap) dbuf_grow(aTHX_ d, n);
805 8           memcpy(d->buf + d->len, s, n);
806 8           d->len += n;
807 8           }
808            
809 4           static void dbuf_indent(pTHX_ dbuf *d, int level) {
810 4           STRLEN need = (STRLEN)level * 2;
811 4 50         if (d->len + need > d->cap) dbuf_grow(aTHX_ d, need);
812 4 50         for (int i = 0; i < level; i++) {
813 0           d->buf[d->len++] = ' ';
814 0           d->buf[d->len++] = ' ';
815             }
816 4           }
817            
818             /* Bare-key check: ASCII identifier ([A-Za-z_][A-Za-z0-9_-]*). For full
819             * Unicode XID coverage we'd need utf8proc; the realistic fixture is
820             * 100% ASCII keys so the ASCII path covers it. Non-bare keys go through
821             * the quoted-key path. */
822 4           static int is_bare_key_ascii(const char *s, STRLEN n) {
823 4 50         if (n == 0) return 0;
824 4           unsigned char c = (unsigned char)s[0];
825 4 50         if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'))
    50          
    50          
    50          
    0          
826 0           return 0;
827 18 100         for (STRLEN i = 1; i < n; i++) {
828 14           c = (unsigned char)s[i];
829 14 50         if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
    50          
    50          
    50          
    0          
    0          
830 0 0         (c >= '0' && c <= '9') || c == '_' || c == '-'))
    0          
831 0           return 0;
832             }
833 4           return 1;
834             }
835            
836             /* Emit a basic-quoted string ("...") with escapes. Fast path for clean
837             * strings copies bytes verbatim. */
838 0           static void dbuf_basic_string(pTHX_ dbuf *d, const char *s, STRLEN n) {
839             /* Fast path: scan for any byte needing escape. */
840 0           int dirty = 0;
841 0 0         for (STRLEN i = 0; i < n; i++) {
842 0           unsigned char c = (unsigned char)s[i];
843 0 0         if (c == '\\' || c == '"' || c < 0x20) { dirty = 1; break; }
    0          
    0          
844             }
845 0 0         if (d->len + n + 2 > d->cap) dbuf_grow(aTHX_ d, n + 2);
846 0           d->buf[d->len++] = '"';
847 0 0         if (!dirty) {
848 0           memcpy(d->buf + d->len, s, n);
849 0           d->len += n;
850 0 0         if (d->len + 1 > d->cap) dbuf_grow(aTHX_ d, 1);
851 0           d->buf[d->len++] = '"';
852 0           return;
853             }
854 0 0         for (STRLEN i = 0; i < n; i++) {
855 0           unsigned char c = (unsigned char)s[i];
856 0 0         if (d->len + 6 > d->cap) dbuf_grow(aTHX_ d, 6);
857 0           switch (c) {
858 0           case '\\': d->buf[d->len++] = '\\'; d->buf[d->len++] = '\\'; break;
859 0           case '"': d->buf[d->len++] = '\\'; d->buf[d->len++] = '"'; break;
860 0           case '\n': d->buf[d->len++] = '\\'; d->buf[d->len++] = 'n'; break;
861 0           case '\r': d->buf[d->len++] = '\\'; d->buf[d->len++] = 'r'; break;
862 0           case '\t': d->buf[d->len++] = '\\'; d->buf[d->len++] = 't'; break;
863 0           case '\b': d->buf[d->len++] = '\\'; d->buf[d->len++] = 'b'; break;
864 0           case '\f': d->buf[d->len++] = '\\'; d->buf[d->len++] = 'f'; break;
865 0           default:
866 0 0         if (c < 0x20) {
867 0           int k = snprintf(d->buf + d->len, d->cap - d->len,
868             "\\u%04X", c);
869 0           d->len += (STRLEN)k;
870             } else {
871 0           d->buf[d->len++] = (char)c;
872             }
873             }
874             }
875 0           d->buf[d->len++] = '"';
876             }
877            
878             /* Emit a key bare-or-quoted. */
879 4           static void dbuf_emit_key(pTHX_ dbuf *d, const char *s, STRLEN n) {
880 4 50         if (is_bare_key_ascii(s, n)) {
881 4           dbuf_puts(aTHX_ d, s, n);
882             } else {
883 0           dbuf_basic_string(aTHX_ d, s, n);
884             }
885 4           }
886            
887             /* Lite-mode tables carry their key order in a sidecar AV stored at
888             * key "\0__dms_keys". Returns that AV if present, NULL otherwise.
889             * Used by emit_perl_table to iterate in insertion order without
890             * incurring Tie::IxHash overhead. */
891 0           static AV *get_lite_keys(pTHX_ HV *hv) {
892 0           SV **slot = hv_fetch(hv, SIDECAR_KEY, SIDECAR_KEY_LEN, 0);
893 0 0         if (!slot || !*slot || !SvROK(*slot)) return NULL;
    0          
    0          
894 0           SV *rv = SvRV(*slot);
895 0 0         if (SvTYPE(rv) != SVt_PVAV) return NULL;
896 0           return (AV *)rv;
897             }
898            
899             /* Detect Tie::IxHash on an HV. Returns the underlying tied AV
900             * `[idx_rv, keys_rv, vals_rv, iter]` if found, NULL otherwise. */
901 2           static AV *get_ixhash_tied(pTHX_ HV *hv) {
902 2 50         if (!SvRMAGICAL((SV *)hv)) return NULL;
903             /* mg_find (not mg_findext) since sv_magic auto-installs a vtable
904             * for PERL_MAGIC_tied; we don't want to match on a NULL vtable. */
905 2           MAGIC *mg = mg_find((SV *)hv, PERL_MAGIC_tied);
906 2 50         if (!mg || !mg->mg_obj) return NULL;
    50          
907 2           SV *obj = mg->mg_obj;
908 2 50         if (!SvROK(obj)) return NULL;
909 2           SV *inner = SvRV(obj);
910 2 50         if (!SvOBJECT(inner)) return NULL;
911 2           HV *tied_stash = SvSTASH(inner);
912 2 50         if (!tied_stash) return NULL;
913 2 50         if (!stash_IxHash) stash_IxHash = gv_stashpv("Tie::IxHash", GV_ADD);
914 2 50         if (tied_stash != stash_IxHash) return NULL;
915 2 50         if (SvTYPE(inner) != SVt_PVAV) return NULL;
916 2           return (AV *)inner;
917             }
918            
919             /* Forward-declare: emit value (any kind) at given indent. */
920             static void emit_perl_value(pTHX_ dbuf *d, SV *v, int indent);
921             static void emit_perl_table(pTHX_ dbuf *d, HV *hv, int indent);
922             static void emit_perl_list(pTHX_ dbuf *d, AV *av, int indent);
923            
924             /* Classify a blessed SV by its stash. */
925             typedef enum {
926             BLE_OTHER = 0,
927             BLE_BOOL, BLE_INTEGER, BLE_FLOAT,
928             BLE_OFFSET_DT, BLE_LOCAL_DT, BLE_LOCAL_DATE, BLE_LOCAL_TIME,
929             BLE_UNORDERED
930             } blessed_kind;
931            
932             static HV *stash_UnorderedTable;
933            
934 8           static blessed_kind classify_blessed(pTHX_ SV *rv) {
935 8 50         if (!SvROK(rv)) return BLE_OTHER;
936 8           SV *target = SvRV(rv);
937 8 50         if (!SvOBJECT(target)) return BLE_OTHER;
938 8           HV *st = SvSTASH(target);
939 8 50         if (!st) return BLE_OTHER;
940             /* Cache the stashes we care about and compare by pointer — much
941             * faster than HvNAME comparisons. */
942 8 100         if (!stash_Bool) stash_Bool = gv_stashpv("DMS::Bool", GV_ADD);
943 8 50         if (st == stash_Bool) return BLE_BOOL;
944 8 50         if (!stash_Integer) stash_Integer = gv_stashpv("DMS::Integer", GV_ADD);
945 8 50         if (st == stash_Integer) return BLE_INTEGER;
946 0 0         if (!stash_Float) stash_Float = gv_stashpv("DMS::Float", GV_ADD);
947 0 0         if (st == stash_Float) return BLE_FLOAT;
948 0 0         if (!stash_OffsetDateTime) stash_OffsetDateTime = gv_stashpv("DMS::OffsetDateTime", GV_ADD);
949 0 0         if (st == stash_OffsetDateTime) return BLE_OFFSET_DT;
950 0 0         if (!stash_LocalDateTime) stash_LocalDateTime = gv_stashpv("DMS::LocalDateTime", GV_ADD);
951 0 0         if (st == stash_LocalDateTime) return BLE_LOCAL_DT;
952 0 0         if (!stash_LocalDate) stash_LocalDate = gv_stashpv("DMS::LocalDate", GV_ADD);
953 0 0         if (st == stash_LocalDate) return BLE_LOCAL_DATE;
954 0 0         if (!stash_LocalTime) stash_LocalTime = gv_stashpv("DMS::LocalTime", GV_ADD);
955 0 0         if (st == stash_LocalTime) return BLE_LOCAL_TIME;
956 0 0         if (!stash_UnorderedTable) stash_UnorderedTable = gv_stashpv("DMS::UnorderedTable", GV_ADD);
957 0 0         if (st == stash_UnorderedTable) return BLE_UNORDERED;
958 0           return BLE_OTHER;
959             }
960            
961             /* Emit a value inline (no indent prefix, no trailing newline).
962             * Used for scalar values and flow-form sub-values. */
963 4           static void emit_perl_value_inline(pTHX_ dbuf *d, SV *v) {
964 4 50         if (!v || !SvOK(v)) {
    50          
965             /* Defensive: undef shouldn't reach here in lite mode (no nulls
966             * in DMS), but emit "" to avoid crashing on broken input. */
967 0           dbuf_puts(aTHX_ d, "\"\"", 2);
968 4           return;
969             }
970 4 50         if (SvROK(v)) {
971 4           blessed_kind k = classify_blessed(aTHX_ v);
972 4 50         if (k == BLE_BOOL) {
973 0           SV *inner = SvRV(v);
974 0 0         if (SvTRUE(inner)) dbuf_puts(aTHX_ d, "true", 4);
975 0           else dbuf_puts(aTHX_ d, "false", 5);
976 0           return;
977             }
978 4 50         if (k == BLE_INTEGER) {
979 4           SV *inner = SvRV(v);
980             char tmp[24];
981 4           int n = snprintf(tmp, sizeof(tmp), "%" IVdf, SvIV(inner));
982 4           dbuf_puts(aTHX_ d, tmp, (STRLEN)n);
983 4           return;
984             }
985 0 0         if (k == BLE_FLOAT) {
986 0           SV *inner = SvRV(v);
987 0           NV f = SvNV(inner);
988 0 0         if (Perl_isnan(f)) { dbuf_puts(aTHX_ d, "nan", 3); return; }
989 0 0         if (Perl_isinf(f)) {
990 0 0         dbuf_puts(aTHX_ d, f > 0 ? "inf" : "-inf", f > 0 ? 3 : 4);
    0          
991 0           return;
992             }
993             char tmp[64];
994 0           int n = snprintf(tmp, sizeof(tmp), "%.17g", f);
995             /* Trim to shortest round-trip would require ryu; the .17g
996             * fallback is correct, just verbose. The lite-mode emit
997             * doesn't promise minimal-form floats anyway. */
998 0           dbuf_puts(aTHX_ d, tmp, (STRLEN)n);
999 0           return;
1000             }
1001 0 0         if (k >= BLE_OFFSET_DT && k <= BLE_LOCAL_TIME) {
    0          
1002 0           SV *inner = SvRV(v);
1003             STRLEN slen;
1004 0           const char *s = SvPV(inner, slen);
1005 0           dbuf_puts(aTHX_ d, s, slen);
1006 0           return;
1007             }
1008 0           SV *rv = SvRV(v);
1009 0 0         if (SvTYPE(rv) == SVt_PVAV) {
1010             /* Plain array ref → flow list. */
1011 0           AV *av = (AV *)rv;
1012 0           SSize_t n = av_len(av) + 1;
1013 0 0         if (n == 0) { dbuf_puts(aTHX_ d, "[]", 2); return; }
1014 0           dbuf_putc(aTHX_ d, '[');
1015 0 0         for (SSize_t i = 0; i < n; i++) {
1016 0 0         if (i > 0) dbuf_puts(aTHX_ d, ", ", 2);
1017 0           SV **slot = av_fetch(av, i, 0);
1018 0 0         if (slot) emit_perl_value_inline(aTHX_ d, *slot);
1019             }
1020 0           dbuf_putc(aTHX_ d, ']');
1021 0           return;
1022             }
1023 0 0         if (SvTYPE(rv) == SVt_PVHV) {
1024             /* Tie::IxHash, lite-mode sidecar, or plain HV. Check
1025             * Tie::IxHash first — hv_fetch (used by get_lite_keys) on
1026             * a tied HV runs FETCH magic, which is slow. */
1027 0           HV *hv = (HV *)rv;
1028 0           AV *tied = get_ixhash_tied(aTHX_ hv);
1029 0 0         AV *lite_keys = tied ? NULL : get_lite_keys(aTHX_ hv);
1030             int empty;
1031 0 0         if (tied) {
1032 0           SV **slot = av_fetch(tied, 1, 0);
1033 0 0         AV *keys = slot ? (AV *)SvRV(*slot) : NULL;
1034 0 0         empty = !keys || av_len(keys) < 0;
    0          
1035 0 0         } else if (lite_keys) {
1036 0           empty = av_len(lite_keys) < 0;
1037             } else {
1038 0 0         empty = !HvUSEDKEYS(hv);
1039             }
1040 0 0         if (empty) { dbuf_puts(aTHX_ d, "{}", 2); return; }
1041 0           dbuf_putc(aTHX_ d, '{');
1042 0           int first = 1;
1043 0 0         if (lite_keys) {
1044 0           SSize_t n = av_len(lite_keys) + 1;
1045 0 0         for (SSize_t i = 0; i < n; i++) {
1046 0           SV **kp = av_fetch(lite_keys, i, 0);
1047 0 0         if (!kp) continue;
1048             STRLEN klen;
1049 0           const char *ks = SvPV(*kp, klen);
1050 0           SV **vp = hv_fetch(hv, ks, (I32)klen, 0);
1051 0 0         if (!vp) continue;
1052 0 0         if (!first) dbuf_puts(aTHX_ d, ", ", 2);
1053 0           first = 0;
1054 0           dbuf_emit_key(aTHX_ d, ks, klen);
1055 0           dbuf_puts(aTHX_ d, ": ", 2);
1056 0           emit_perl_value_inline(aTHX_ d, *vp);
1057             }
1058 0 0         } else if (tied) {
1059 0           SV **k_slot = av_fetch(tied, 1, 0);
1060 0           SV **v_slot = av_fetch(tied, 2, 0);
1061 0           AV *keys = (AV *)SvRV(*k_slot);
1062 0           AV *vals = (AV *)SvRV(*v_slot);
1063 0           SSize_t n = av_len(keys) + 1;
1064 0 0         for (SSize_t i = 0; i < n; i++) {
1065 0           SV **kp = av_fetch(keys, i, 0);
1066 0           SV **vp = av_fetch(vals, i, 0);
1067 0 0         if (!kp || !vp) continue;
    0          
1068 0 0         if (!first) dbuf_puts(aTHX_ d, ", ", 2);
1069 0           first = 0;
1070             STRLEN klen;
1071 0           const char *ks = SvPV(*kp, klen);
1072 0           dbuf_emit_key(aTHX_ d, ks, klen);
1073 0           dbuf_puts(aTHX_ d, ": ", 2);
1074 0           emit_perl_value_inline(aTHX_ d, *vp);
1075             }
1076             } else {
1077 0           hv_iterinit(hv);
1078             HE *he;
1079 0 0         while ((he = hv_iternext(hv))) {
1080 0 0         if (!first) dbuf_puts(aTHX_ d, ", ", 2);
1081 0           first = 0;
1082             STRLEN klen;
1083 0 0         char *ks = HePV(he, klen);
1084 0           dbuf_emit_key(aTHX_ d, ks, klen);
1085 0           dbuf_puts(aTHX_ d, ": ", 2);
1086 0           emit_perl_value_inline(aTHX_ d, HeVAL(he));
1087             }
1088             }
1089 0           dbuf_putc(aTHX_ d, '}');
1090 0           return;
1091             }
1092             /* Other refs: shouldn't happen in lite mode. Fall through. */
1093 0           dbuf_puts(aTHX_ d, "\"\"", 2);
1094 0           return;
1095             }
1096             /* Plain (non-ref) scalar. In Perl, parse_document_lite returns string
1097             * values as plain SV PVs. */
1098             STRLEN slen;
1099 0           const char *s = SvPV(v, slen);
1100 0           dbuf_basic_string(aTHX_ d, s, slen);
1101             }
1102            
1103 2           static void emit_perl_table(pTHX_ dbuf *d, HV *hv, int indent) {
1104             /* Order matters: check Tie::IxHash FIRST (full-mode common case)
1105             * because hv_fetch on a tied HV runs the FETCH magic, and looking
1106             * up the lite-mode sidecar key first would trigger that on every
1107             * full-mode table. mg_find is a cheap pointer-chain walk by
1108             * comparison. */
1109 2           AV *tied = get_ixhash_tied(aTHX_ hv);
1110 2 50         if (tied) {
1111 2           SV **k_slot = av_fetch(tied, 1, 0);
1112 2           SV **v_slot = av_fetch(tied, 2, 0);
1113 2 50         if (!k_slot || !v_slot) return;
    50          
1114 2           AV *keys = (AV *)SvRV(*k_slot);
1115 2           AV *vals = (AV *)SvRV(*v_slot);
1116 2           SSize_t n = av_len(keys) + 1;
1117 6 100         for (SSize_t i = 0; i < n; i++) {
1118 4           SV **kp = av_fetch(keys, i, 0);
1119 4           SV **vp = av_fetch(vals, i, 0);
1120 4 50         if (!kp || !vp) continue;
    50          
1121             STRLEN klen;
1122 4           const char *ks = SvPV(*kp, klen);
1123 4           SV *v = *vp;
1124 4           int can_block = 0;
1125 4 50         if (v && SvROK(v)) {
    50          
1126 4           SV *rv = SvRV(v);
1127 4           blessed_kind bk = classify_blessed(aTHX_ v);
1128 4 50         if (bk == BLE_OTHER || bk == BLE_UNORDERED) {
    50          
1129 0 0         if (SvTYPE(rv) == SVt_PVHV) {
1130 0           HV *sub = (HV *)rv;
1131 0           AV *sub_tied = get_ixhash_tied(aTHX_ sub);
1132 0 0         if (sub_tied) {
1133 0           SV **sk = av_fetch(sub_tied, 1, 0);
1134 0 0         if (sk && av_len((AV *)SvRV(*sk)) >= 0)
    0          
1135 0           can_block = 1;
1136 0 0         } else if (HvUSEDKEYS(sub)) {
    0          
1137 0           can_block = 1;
1138             }
1139 0 0         } else if (SvTYPE(rv) == SVt_PVAV) {
1140 0 0         if (av_len((AV *)rv) >= 0) can_block = 1;
1141             }
1142             }
1143             }
1144 4           dbuf_indent(aTHX_ d, indent);
1145 4           dbuf_emit_key(aTHX_ d, ks, klen);
1146 4           dbuf_putc(aTHX_ d, ':');
1147 4 50         if (can_block) {
1148 0           dbuf_putc(aTHX_ d, '\n');
1149 0           SV *rv = SvRV(v);
1150 0 0         if (SvTYPE(rv) == SVt_PVHV) {
1151 0           emit_perl_table(aTHX_ d, (HV *)rv, indent + 1);
1152             } else {
1153 0           emit_perl_list(aTHX_ d, (AV *)rv, indent + 1);
1154             }
1155             } else {
1156 4           dbuf_putc(aTHX_ d, ' ');
1157 4           emit_perl_value_inline(aTHX_ d, v);
1158 4           dbuf_putc(aTHX_ d, '\n');
1159             }
1160             }
1161 2           return;
1162             }
1163             /* Plain HV: lite-mode shape with sidecar __dms_keys, OR a non-DMS
1164             * hash. The sidecar lookup is safe now (no tie magic to dispatch). */
1165 0           AV *lite_keys = get_lite_keys(aTHX_ hv);
1166 0 0         if (lite_keys) {
1167 0           SSize_t n = av_len(lite_keys) + 1;
1168 0 0         for (SSize_t i = 0; i < n; i++) {
1169 0           SV **kp = av_fetch(lite_keys, i, 0);
1170 0 0         if (!kp) continue;
1171             STRLEN klen;
1172 0           const char *ks = SvPV(*kp, klen);
1173 0           SV **vp = hv_fetch(hv, ks, (I32)klen, 0);
1174 0 0         if (!vp) continue;
1175 0           SV *v = *vp;
1176 0           int can_block = 0;
1177 0 0         if (v && SvROK(v)) {
    0          
1178 0           SV *rv = SvRV(v);
1179 0           blessed_kind bk = classify_blessed(aTHX_ v);
1180 0 0         if (bk == BLE_OTHER || bk == BLE_UNORDERED) {
    0          
1181 0 0         if (SvTYPE(rv) == SVt_PVHV) {
1182 0           HV *sub = (HV *)rv;
1183             /* Sub-table check: lite_keys path again. mg_find on a
1184             * non-tied sub HV is fast; sidecar check fast too. */
1185 0 0         if (SvRMAGICAL((SV *)sub) && get_ixhash_tied(aTHX_ sub)) {
    0          
1186 0           AV *sub_tied = get_ixhash_tied(aTHX_ sub);
1187 0           SV **sk = av_fetch(sub_tied, 1, 0);
1188 0 0         if (sk && av_len((AV *)SvRV(*sk)) >= 0)
    0          
1189 0           can_block = 1;
1190             } else {
1191 0           AV *sub_keys = get_lite_keys(aTHX_ sub);
1192 0 0         if (sub_keys) {
1193 0 0         if (av_len(sub_keys) >= 0) can_block = 1;
1194 0 0         } else if (HvUSEDKEYS(sub)) {
    0          
1195 0           can_block = 1;
1196             }
1197             }
1198 0 0         } else if (SvTYPE(rv) == SVt_PVAV) {
1199 0 0         if (av_len((AV *)rv) >= 0) can_block = 1;
1200             }
1201             }
1202             }
1203 0           dbuf_indent(aTHX_ d, indent);
1204 0           dbuf_emit_key(aTHX_ d, ks, klen);
1205 0           dbuf_putc(aTHX_ d, ':');
1206 0 0         if (can_block) {
1207 0           dbuf_putc(aTHX_ d, '\n');
1208 0           SV *rv = SvRV(v);
1209 0 0         if (SvTYPE(rv) == SVt_PVHV) emit_perl_table(aTHX_ d, (HV *)rv, indent + 1);
1210 0           else emit_perl_list(aTHX_ d, (AV *)rv, indent + 1);
1211             } else {
1212 0           dbuf_putc(aTHX_ d, ' ');
1213 0           emit_perl_value_inline(aTHX_ d, v);
1214 0           dbuf_putc(aTHX_ d, '\n');
1215             }
1216             }
1217 0           return;
1218             }
1219             /* Plain HV without sidecar. Iteration order arbitrary. */
1220 0           hv_iterinit(hv);
1221             HE *he;
1222 0 0         while ((he = hv_iternext(hv))) {
1223             STRLEN klen;
1224 0 0         char *ks = HePV(he, klen);
1225 0           SV *v = HeVAL(he);
1226 0           int can_block = 0;
1227 0 0         if (v && SvROK(v)) {
    0          
1228 0           SV *rv = SvRV(v);
1229 0           blessed_kind bk = classify_blessed(aTHX_ v);
1230 0 0         if (bk == BLE_OTHER || bk == BLE_UNORDERED) {
    0          
1231 0 0         if (SvTYPE(rv) == SVt_PVHV) {
1232 0 0         if (HvUSEDKEYS((HV *)rv)) can_block = 1;
    0          
1233 0 0         } else if (SvTYPE(rv) == SVt_PVAV) {
1234 0 0         if (av_len((AV *)rv) >= 0) can_block = 1;
1235             }
1236             }
1237             }
1238 0           dbuf_indent(aTHX_ d, indent);
1239 0           dbuf_emit_key(aTHX_ d, ks, klen);
1240 0           dbuf_putc(aTHX_ d, ':');
1241 0 0         if (can_block) {
1242 0           dbuf_putc(aTHX_ d, '\n');
1243 0           SV *rv = SvRV(v);
1244 0 0         if (SvTYPE(rv) == SVt_PVHV) emit_perl_table(aTHX_ d, (HV *)rv, indent + 1);
1245 0           else emit_perl_list(aTHX_ d, (AV *)rv, indent + 1);
1246             } else {
1247 0           dbuf_putc(aTHX_ d, ' ');
1248 0           emit_perl_value_inline(aTHX_ d, v);
1249 0           dbuf_putc(aTHX_ d, '\n');
1250             }
1251             }
1252             }
1253            
1254 0           static void emit_perl_list(pTHX_ dbuf *d, AV *av, int indent) {
1255 0           SSize_t n = av_len(av) + 1;
1256 0 0         for (SSize_t i = 0; i < n; i++) {
1257 0           SV **slot = av_fetch(av, i, 0);
1258 0 0         if (!slot) continue;
1259 0           SV *v = *slot;
1260 0           int can_block = 0;
1261 0 0         if (v && SvROK(v)) {
    0          
1262 0           SV *rv = SvRV(v);
1263 0           blessed_kind bk = classify_blessed(aTHX_ v);
1264 0 0         if (bk == BLE_OTHER || bk == BLE_UNORDERED) {
    0          
1265 0 0         if (SvTYPE(rv) == SVt_PVHV) {
1266 0           HV *sub = (HV *)rv;
1267             /* Tie::IxHash check first (cheap mg_find); fall
1268             * through to lite-keys sidecar lookup only on
1269             * non-tied HVs to avoid triggering FETCH magic. */
1270 0           AV *sub_tied = get_ixhash_tied(aTHX_ sub);
1271 0 0         if (sub_tied) {
1272 0           SV **sk = av_fetch(sub_tied, 1, 0);
1273 0 0         if (sk && av_len((AV *)SvRV(*sk)) >= 0)
    0          
1274 0           can_block = 1;
1275             } else {
1276 0           AV *sub_lite_keys = get_lite_keys(aTHX_ sub);
1277 0 0         if (sub_lite_keys) {
1278 0 0         if (av_len(sub_lite_keys) >= 0) can_block = 1;
1279 0 0         } else if (HvUSEDKEYS(sub)) {
    0          
1280 0           can_block = 1;
1281             }
1282             }
1283 0 0         } else if (SvTYPE(rv) == SVt_PVAV) {
1284 0 0         if (av_len((AV *)rv) >= 0) can_block = 1;
1285             }
1286             }
1287             }
1288 0           dbuf_indent(aTHX_ d, indent);
1289 0           dbuf_putc(aTHX_ d, '+');
1290 0 0         if (can_block) {
1291 0           dbuf_putc(aTHX_ d, '\n');
1292 0           SV *rv = SvRV(v);
1293 0 0         if (SvTYPE(rv) == SVt_PVHV) emit_perl_table(aTHX_ d, (HV *)rv, indent + 1);
1294 0           else emit_perl_list(aTHX_ d, (AV *)rv, indent + 1);
1295             } else {
1296 0           dbuf_putc(aTHX_ d, ' ');
1297 0           emit_perl_value_inline(aTHX_ d, v);
1298 0           dbuf_putc(aTHX_ d, '\n');
1299             }
1300             }
1301 0           }
1302            
1303             /* Top-level entry: emit a Document hashref { meta, body } as DMS source.
1304             * For lite-mode benches the input is what parse_document_lite returns. */
1305 2           static SV *to_dms_lite_perl_xs(pTHX_ SV *doc_rv) {
1306 2 50         if (!SvROK(doc_rv) || SvTYPE(SvRV(doc_rv)) != SVt_PVHV) {
    50          
1307 0           croak("to_dms_lite_xs: expected Document hashref");
1308             }
1309 2           HV *doc = (HV *)SvRV(doc_rv);
1310            
1311             dbuf d;
1312 2           dbuf_init(aTHX_ &d, 64 * 1024);
1313            
1314             /* Meta (front matter) — emit `+++ ... +++` block when present. Skip
1315             * floating FM comments in lite mode. */
1316 2           SV **meta_slot = hv_fetch(doc, "meta", 4, 0);
1317 2 50         if (meta_slot && *meta_slot && SvOK(*meta_slot)) {
    50          
    50          
1318 0           SV *meta_sv = *meta_slot;
1319 0 0         if (SvROK(meta_sv) && SvTYPE(SvRV(meta_sv)) == SVt_PVHV) {
    0          
1320 0           HV *meta_hv = (HV *)SvRV(meta_sv);
1321 0           int has_keys = 0;
1322             /* Tie::IxHash check first to avoid FETCH-magic dispatch on
1323             * full-mode docs. Sidecar lookup only on non-tied HVs. */
1324 0           AV *meta_tied = get_ixhash_tied(aTHX_ meta_hv);
1325 0 0         if (meta_tied) {
1326 0           SV **kp = av_fetch(meta_tied, 1, 0);
1327 0 0         if (kp && av_len((AV *)SvRV(*kp)) >= 0) has_keys = 1;
    0          
1328             } else {
1329 0           AV *meta_lite_keys = get_lite_keys(aTHX_ meta_hv);
1330 0 0         if (meta_lite_keys) {
1331 0 0         if (av_len(meta_lite_keys) >= 0) has_keys = 1;
1332 0 0         } else if (HvUSEDKEYS(meta_hv)) {
    0          
1333 0           has_keys = 1;
1334             }
1335             }
1336 0 0         if (has_keys) {
1337 0           dbuf_puts(aTHX_ &d, "+++\n", 4);
1338 0           emit_perl_table(aTHX_ &d, meta_hv, 0);
1339 0           dbuf_puts(aTHX_ &d, "+++\n", 4);
1340             }
1341             }
1342             }
1343            
1344 2           SV **body_slot = hv_fetch(doc, "body", 4, 0);
1345 2 50         if (body_slot && *body_slot) {
    50          
1346 2           SV *body = *body_slot;
1347 2 50         if (SvROK(body)) {
1348 2           SV *body_rv = SvRV(body);
1349 2 50         if (SvTYPE(body_rv) == SVt_PVHV) {
1350 2           emit_perl_table(aTHX_ &d, (HV *)body_rv, 0);
1351 0 0         } else if (SvTYPE(body_rv) == SVt_PVAV) {
1352 0           emit_perl_list(aTHX_ &d, (AV *)body_rv, 0);
1353             } else {
1354 0           emit_perl_value_inline(aTHX_ &d, body);
1355 0           dbuf_putc(aTHX_ &d, '\n');
1356             }
1357 0 0         } else if (SvOK(body)) {
1358 0           emit_perl_value_inline(aTHX_ &d, body);
1359 0           dbuf_putc(aTHX_ &d, '\n');
1360             }
1361             }
1362            
1363 2           SV *out = newSVpvn(d.buf, d.len);
1364 2           SvUTF8_on(out);
1365 2           dbuf_free(aTHX_ &d);
1366 2           return out;
1367             }
1368            
1369            
1370             /* --- XS entry points --------------------------------------------------- */
1371            
1372             MODULE = DMS::XS::Parser PACKAGE = DMS::XS::Parser
1373            
1374             PROTOTYPES: DISABLE
1375            
1376             SV *
1377             parse_document(src_sv)
1378             SV *src_sv
1379             CODE:
1380             STRLEN src_len;
1381 40           const char *src = SvPV(src_sv, src_len);
1382            
1383             dms_error err;
1384 40           dms_document *doc = dms_parse_document(src, src_len, &err);
1385            
1386 40 50         if (!doc) {
1387 0           croak("%d:%d: %s\n", err.line, err.column, err.message);
1388             }
1389            
1390             /* Build { meta, body, comments } hashref. */
1391 40           HV *out = newHV();
1392            
1393             /* meta: undef when no front matter. */
1394 40 100         if (doc->meta) {
1395             dms_value mv;
1396 6           mv.type = DMS_TABLE;
1397 6           mv.u.t = *doc->meta;
1398 6           SV *mv_sv = value_to_sv(aTHX_ &mv);
1399 6           hv_store(out, "meta", 4, mv_sv, 0);
1400             } else {
1401 34           hv_store(out, "meta", 4, newSV(0), 0);
1402             }
1403            
1404 40           hv_store(out, "body", 4, value_to_sv(aTHX_ doc->body), 0);
1405 40           hv_store(out, "comments", 8,
1406             comments_to_sv(aTHX_ doc->comments, doc->num_comments), 0);
1407             /* original_forms: per-path source-lexeme records the Emitter consults
1408             * during full-mode `to_dms` to preserve integer base (0xFF stays
1409             * 0xFF, not 255), string quote style (basic vs literal vs heredoc),
1410             * heredoc label + modifier chain. Empty in lite mode. Without this
1411             * key the Emitter falls back to canonical form for every literal —
1412             * which matched lite-mode behaviour and is what `--mode full`
1413             * regressed to before this fix. */
1414 40           hv_store(out, "original_forms", 14,
1415             original_forms_to_sv(aTHX_ doc->original_forms,
1416             doc->num_original_forms), 0);
1417            
1418 40           dms_document_free(doc);
1419            
1420 40           RETVAL = newRV_noinc((SV *)out);
1421             OUTPUT:
1422             RETVAL
1423            
1424             SV *
1425             parse_to_json_bytes(src_sv)
1426             SV *src_sv
1427             CODE:
1428             /* Parse + serialize-to-canonical-JSON in C, end-to-end. Skips the
1429             SV/HV/AV/Tie::IxHash marshaling of parse_document entirely; for
1430             wide flat documents that's the dominant cost. Returns a Perl
1431             UTF-8 string suitable to print straight to stdout. */
1432             STRLEN src_len;
1433 0           const char *src = SvPV(src_sv, src_len);
1434            
1435             dms_error err;
1436 0           dms_document *doc = dms_parse_document_lite(src, src_len, &err);
1437 0 0         if (!doc) {
1438 0           croak("%d:%d: %s\n", err.line, err.column, err.message);
1439             }
1440            
1441 0           jbuf j; j.buf = NULL; j.len = 0; j.cap = 0;
1442             /* Pre-size: JSON is roughly 5-8x the source for tagged scalars on
1443             wide flat docs. Reserve to avoid early reallocs. */
1444 0           jbuf_grow(&j, src_len * 6 + 1024);
1445 0           emit_document(&j, doc);
1446            
1447 0           dms_document_free(doc);
1448            
1449 0           SV *out = newSVpvn(j.buf, j.len);
1450 0           SvUTF8_on(out);
1451 0           libc_free(j.buf);
1452            
1453 0           RETVAL = out;
1454             OUTPUT:
1455             RETVAL
1456            
1457             void
1458             encode_stdin_to_stdout()
1459             CODE:
1460             /* End-to-end fast path: read STDIN in C, parse, emit tagged JSON, write
1461             to STDOUT in C. Eliminates the two SV<->C buffer copies that the
1462             parse_to_json_bytes path still pays (input slurp into a Perl SV, then
1463             the result SV that the Perl caller `print`s). The conformance-encoder
1464             driver becomes a one-liner.
1465            
1466             On Windows we bypass PerlIO entirely and go straight to the OS via
1467             GetStdHandle + ReadFile/WriteFile. PerlIO_read has a small internal
1468             buffer (~4 KB) that costs measurable time on a 700 KB slurp;
1469             PerlIO_write fragments the 1.6 MB output across many buffered
1470             writes. The OS-level path matches what dms-encoder.exe does and
1471             closes the gap to the native baseline. Caller must `binmode STDIN`
1472             and `binmode STDOUT, ":raw"` first so that Perl doesn't introduce
1473             CRLF translation we'd be skipping past. */
1474             {
1475             #if defined(WIN32) || defined(_WIN32)
1476             HANDLE hin = GetStdHandle(STD_INPUT_HANDLE);
1477             HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
1478             #endif
1479            
1480             /* Slurp stdin via a growable libc buffer. Sized to absorb typical
1481             conformance docs in one read; doubles on overflow. */
1482 0           size_t in_cap = 65536;
1483 0           size_t in_len = 0;
1484 0           char *in_buf = (char *)libc_realloc(NULL, in_cap);
1485 0 0         if (!in_buf) croak("out of memory reading stdin");
1486 0           for (;;) {
1487 0 0         if (in_len == in_cap) {
1488 0           in_cap *= 2;
1489 0           in_buf = (char *)libc_realloc(in_buf, in_cap);
1490 0 0         if (!in_buf) croak("out of memory reading stdin");
1491             }
1492             #if defined(WIN32) || defined(_WIN32)
1493             DWORD got = 0;
1494             BOOL ok = ReadFile(hin, in_buf + in_len, (DWORD)(in_cap - in_len),
1495             &got, NULL);
1496             if (!ok || got == 0) break;
1497             in_len += (size_t)got;
1498             #else
1499 0           ssize_t got = read(0, in_buf + in_len, in_cap - in_len);
1500 0 0         if (got <= 0) break;
1501 0           in_len += (size_t)got;
1502             #endif
1503             }
1504            
1505             dms_error err;
1506 0           dms_document *doc = dms_parse_document_lite(in_buf, in_len, &err);
1507 0 0         if (!doc) {
1508 0           libc_free(in_buf);
1509 0           croak("%d:%d: %s\n", err.line, err.column, err.message);
1510             }
1511            
1512 0           jbuf j; j.buf = NULL; j.len = 0; j.cap = 0;
1513 0           jbuf_grow(&j, in_len * 6 + 1024);
1514 0           emit_document(&j, doc);
1515            
1516 0           dms_document_free(doc);
1517 0           libc_free(in_buf);
1518            
1519             /* Bulk write straight to the OS handle. */
1520 0           size_t wrote = 0;
1521 0 0         while (wrote < j.len) {
1522             #if defined(WIN32) || defined(_WIN32)
1523             DWORD n = 0;
1524             BOOL ok = WriteFile(hout, j.buf + wrote, (DWORD)(j.len - wrote),
1525             &n, NULL);
1526             if (!ok || n == 0) {
1527             libc_free(j.buf);
1528             croak("write to stdout failed");
1529             }
1530             wrote += n;
1531             #else
1532 0           ssize_t n = write(1, j.buf + wrote, j.len - wrote);
1533 0 0         if (n <= 0) { libc_free(j.buf); croak("write to stdout failed"); }
1534 0           wrote += (size_t)n;
1535             #endif
1536             }
1537 0           libc_free(j.buf);
1538             }
1539            
1540             SV *
1541             parse_document_lite(src_sv)
1542             SV *src_sv
1543             CODE:
1544             /* Lite-mode parse: same data tree as parse_document, but with two
1545             * shape changes that drop tree-construction cost:
1546             * - Tables are plain HVs with a sidecar `__dms_keys` AV at key
1547             * "\0__dms_keys" instead of Tie::IxHash. Saves ~6 SVs/table.
1548             * - Comment AST and original_forms are skipped (lite contract).
1549             * SPEC §Parsing modes — full and lite. */
1550             STRLEN src_len;
1551 8           const char *src = SvPV(src_sv, src_len);
1552            
1553             dms_error err;
1554 8           dms_document *doc = dms_parse_document_lite(src, src_len, &err);
1555            
1556 8 100         if (!doc) {
1557 4           croak("%d:%d: %s\n", err.line, err.column, err.message);
1558             }
1559            
1560 4           HV *out = newHV();
1561 4 50         if (doc->meta) {
1562             dms_value mv;
1563 4           mv.type = DMS_TABLE;
1564 4           mv.u.t = *doc->meta;
1565 4           hv_store(out, "meta", 4, value_to_sv_lite(aTHX_ &mv), 0);
1566             } else {
1567 0           hv_store(out, "meta", 4, newSV(0), 0);
1568             }
1569 4           hv_store(out, "body", 4, value_to_sv_lite(aTHX_ doc->body), 0);
1570 4           hv_store(out, "comments", 8, newRV_noinc((SV *)newAV()), 0);
1571            
1572 4           dms_document_free(doc);
1573            
1574 4           RETVAL = newRV_noinc((SV *)out);
1575             OUTPUT:
1576             RETVAL
1577            
1578            
1579             SV *
1580             to_dms_lite_xs(doc_rv)
1581             SV *doc_rv
1582             CODE:
1583             /* C-side to_dms_lite — walks the Perl Document tree (the shape
1584             * parse_document_lite returns) and emits canonical DMS source
1585             * directly into a Perl SV. Skips the per-kvpair Perl-VM trips of
1586             * the pure-Perl Emitter. Lite-mode only — no comment AST, no
1587             * original_forms preservation. SPEC §to_dms (canonical-form
1588             * subset). */
1589 2           RETVAL = to_dms_lite_perl_xs(aTHX_ doc_rv);
1590             OUTPUT:
1591             RETVAL