yeptris 0.6.10.1 → 0.6.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 911e4f9b49027d9a90c269d485980d025040866830e8138d6dbb7e573e6e49ca
4
- data.tar.gz: c48b0a777f5e7f7f3771bdb44661f7a5d3f11e3713895b3d0029847ce56cb7ef
3
+ metadata.gz: fab04bdd5f0236d59b5d139d410c095d9c99c8aacccaa18c464324c58b0eef33
4
+ data.tar.gz: 9ba0ca96dd534843e8238921f3de634c7f18b051f90b3bde901be74b950a6403
5
5
  SHA512:
6
- metadata.gz: cf0d8e4fb6c0833b3a1232293cc55ce4e51e4a88af05b84eee940288c197eaf2c837c3f75382cdcb643a64680869e314ae09b3076b711953cd40cd244535aedb
7
- data.tar.gz: 3276f1b2af11b03a225b0b52bc7bc38982c38a65fe50b3890ab7a02d9d8c202a7e2703efc8879b8d96e9f51483e01115961c263fc89093acf01733ca71f0d454
6
+ metadata.gz: 2281939709069aaf555ed5df307cc613b304ad236d7c3fc13cca8a4fc28a0ee8ca35f41d7dd3b46c9db71a8a088d90565e4ccfd7aa25c4512a83d46b41bddd79
7
+ data.tar.gz: 9731da1240594c945e1eba4f7eef86e3ec10556c23ab0c84617b2109c17c2c08e08d2c64c5cd558103c07abe2ef93da00735ca6c42703e61fbae4eac7a0ad64c
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Yeptris
4
4
  # The gem's version lives in the parent namespace's file — the last
5
5
  # internal require (yeptris/version) retired with it.
6
- VERSION = "0.6.10.1".freeze
6
+ VERSION = "0.6.11.1".freeze
7
7
  # The error hierarchy lives in THIS file (the parent namespace's
8
8
  # own file): nested constants do not trigger a parent-constant
9
9
  # autoload, and the law forbids internal requires — defining the
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.10
6
+ VERSION 0.6.11
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -39,6 +39,11 @@ YEPTRIS_API size_t yeptris_serialize_into_ex(YeptrisDocument handle,
39
39
  return 0;
40
40
  }
41
41
  yep_emitter em;
42
+ em.w.sc_dec = NULL;
43
+ em.w.sc_i = 0;
44
+ em.w.cap = 0;
45
+ em.w.grow = 0;
46
+ em.w.oom = 0;
42
47
  em.doc = (const yeptris_document*)handle;
43
48
  em.w.p = NULL;
44
49
  em.w.last = 0;
@@ -60,13 +65,20 @@ YEPTRIS_API size_t yeptris_serialize_into_ex(YeptrisDocument handle,
60
65
  if (!yep_nametab_init(&em.canon_names, yep_system_allocator())) {
61
66
  return 0;
62
67
  }
68
+ em.w.sc_dec = (uint8_t*)malloc(em.doc->dom->ncount > 0 ? em.doc->dom->ncount : 1);
69
+ if (em.w.sc_dec == NULL) {
70
+ yep_nametab_free(&em.canon_names);
71
+ return 0;
72
+ }
63
73
  size_t need = yep_emit_run(&em, 1);
64
74
  if (buf == NULL || cap < need + 1) {
75
+ free(em.w.sc_dec);
65
76
  yep_nametab_free(&em.canon_names);
66
77
  return need;
67
78
  }
68
79
  em.w.p = buf;
69
80
  size_t wrote = yep_emit_run(&em, 0);
81
+ free(em.w.sc_dec);
70
82
  yep_nametab_free(&em.canon_names);
71
83
  buf[wrote] = '\0';
72
84
  return wrote;
@@ -82,6 +94,11 @@ YEPTRIS_API char* yeptris_serialize_ex(YeptrisDocument handle, const yeptris_emi
82
94
  return NULL;
83
95
  }
84
96
  yep_emitter em;
97
+ em.w.sc_dec = NULL;
98
+ em.w.sc_i = 0;
99
+ em.w.cap = 0;
100
+ em.w.grow = 0;
101
+ em.w.oom = 0;
85
102
  em.doc = (const yeptris_document*)handle;
86
103
  em.w.p = NULL;
87
104
  em.w.last = 0;
@@ -103,13 +120,41 @@ YEPTRIS_API char* yeptris_serialize_ex(YeptrisDocument handle, const yeptris_emi
103
120
  if (!yep_nametab_init(&em.canon_names, yep_system_allocator())) {
104
121
  return NULL;
105
122
  }
106
- size_t need = yep_emit_run(&em, 1);
107
- char* out = malloc(need + 1);
123
+ /* #352 slice 2: ONE wet pass into a growable buffer — the dry
124
+ * exact-sizing pass measured 0.54-0.73x of this route on CI-fresh
125
+ * runners (the emit-split table's 2p/1p column). The estimate
126
+ * starts at the input size (+64) and doubles on demand; a final
127
+ * shrink-to-fit keeps the returned allocation tight. No dry pass,
128
+ * so no decisions table. */
129
+ size_t est = em.doc->dom->input_len + 64;
130
+ if (est < 256) {
131
+ est = 256;
132
+ }
133
+ char* out = (char*)malloc(est);
108
134
  if (out == NULL) {
135
+ yep_nametab_free(&em.canon_names);
109
136
  return NULL;
110
137
  }
111
138
  em.w.p = out;
139
+ em.w.cap = est;
140
+ em.w.grow = 1;
112
141
  size_t wrote = yep_emit_run(&em, 0);
142
+ em.w.grow = 0;
143
+ /* wr_grow reallocs through w->p — the local `out` is stale after
144
+ * any growth; every post-run touch goes through the writer's
145
+ * pointer (the first round's abort was exactly this aliasing) */
146
+ out = em.w.p;
147
+ if (em.w.oom) {
148
+ free(out);
149
+ yep_nametab_free(&em.canon_names);
150
+ return NULL;
151
+ }
152
+ if (wrote + 1 < em.w.cap) {
153
+ char* tight = (char*)realloc(out, wrote + 1);
154
+ if (tight != NULL) {
155
+ out = tight;
156
+ }
157
+ }
113
158
  yep_nametab_free(&em.canon_names);
114
159
  out[wrote] = '\0';
115
160
  if (len != NULL) {
@@ -123,6 +168,11 @@ YEPTRIS_API char* yeptris_serialize_json(YeptrisDocument handle, size_t* len) {
123
168
  return NULL;
124
169
  }
125
170
  yep_emitter em;
171
+ em.w.sc_dec = NULL;
172
+ em.w.sc_i = 0;
173
+ em.w.cap = 0;
174
+ em.w.grow = 0;
175
+ em.w.oom = 0;
126
176
  em.doc = (const yeptris_document*)handle;
127
177
  em.w.p = NULL;
128
178
  em.w.last = 0;
@@ -168,6 +218,11 @@ YEPTRIS_API char* yeptris_serialize_json_ex(YeptrisDocument handle, size_t* len,
168
218
  return NULL;
169
219
  }
170
220
  yep_emitter em;
221
+ em.w.sc_dec = NULL;
222
+ em.w.sc_i = 0;
223
+ em.w.cap = 0;
224
+ em.w.grow = 0;
225
+ em.w.oom = 0;
171
226
  em.doc = (const yeptris_document*)handle;
172
227
  em.w.p = NULL;
173
228
  em.w.last = 0;
@@ -210,6 +265,11 @@ char* yep_serialize_json_compact(const yeptris_document* doc, size_t* len) {
210
265
  return NULL;
211
266
  }
212
267
  yep_emitter em;
268
+ em.w.sc_dec = NULL;
269
+ em.w.sc_i = 0;
270
+ em.w.cap = 0;
271
+ em.w.grow = 0;
272
+ em.w.oom = 0;
213
273
  em.doc = doc;
214
274
  em.w.p = NULL;
215
275
  em.w.last = 0;
@@ -252,6 +312,11 @@ char* yep_serialize_json_pretty(const yeptris_document* doc, size_t* len) {
252
312
  return NULL;
253
313
  }
254
314
  yep_emitter em;
315
+ em.w.sc_dec = NULL;
316
+ em.w.sc_i = 0;
317
+ em.w.cap = 0;
318
+ em.w.grow = 0;
319
+ em.w.oom = 0;
255
320
  em.doc = doc;
256
321
  em.w.p = NULL;
257
322
  em.w.last = 0;
@@ -307,6 +372,11 @@ YEPTRIS_API size_t yeptris_serialize_stream(YeptrisDocument handle,
307
372
  return 0;
308
373
  }
309
374
  yep_emitter em;
375
+ em.w.sc_dec = NULL;
376
+ em.w.sc_i = 0;
377
+ em.w.cap = 0;
378
+ em.w.grow = 0;
379
+ em.w.oom = 0;
310
380
  em.doc = (const yeptris_document*)handle;
311
381
  em.w.p = NULL;
312
382
  em.w.last = 0;
@@ -64,6 +64,24 @@ static void wr_col_update(yep_writer* w, const char* p, uint32_t n) {
64
64
  w->col += (int)n;
65
65
  }
66
66
 
67
+ static void wr_grow(yep_writer* w, size_t need) {
68
+ if (w->oom || w->p == NULL) {
69
+ w->oom = w->p == NULL;
70
+ return;
71
+ }
72
+ size_t want = w->len + need + 1;
73
+ if (want < w->cap * 2) {
74
+ want = w->cap * 2;
75
+ }
76
+ char* np = (char*)realloc(w->p, want);
77
+ if (np == NULL) {
78
+ w->oom = 1;
79
+ return;
80
+ }
81
+ w->p = np;
82
+ w->cap = want;
83
+ }
84
+
67
85
  static void wr_put(yep_writer* w, const char* p, uint32_t n) {
68
86
  if (n > 0) {
69
87
  w->last = p[n - 1];
@@ -73,6 +91,13 @@ static void wr_put(yep_writer* w, const char* p, uint32_t n) {
73
91
  w->len += n;
74
92
  return;
75
93
  }
94
+ if (w->grow && w->len + n > w->cap) {
95
+ wr_grow(w, n);
96
+ }
97
+ if (w->oom) {
98
+ w->len += n; /* keep counting: the caller fails on oom */
99
+ return;
100
+ }
76
101
  memcpy(w->p + w->len, p, n);
77
102
  w->len += n;
78
103
  wr_maybe_flush(w);
@@ -89,6 +114,13 @@ static void wr_byte(yep_writer* w, char c) {
89
114
  w->len++;
90
115
  return;
91
116
  }
117
+ if (w->grow && w->len + 1 > w->cap) {
118
+ wr_grow(w, 1);
119
+ }
120
+ if (w->oom) {
121
+ w->len++;
122
+ return;
123
+ }
92
124
  w->p[w->len] = c;
93
125
  w->len++;
94
126
  wr_maybe_flush(w);
@@ -275,21 +307,16 @@ static void emit_canonical_scalar(yep_writer* w, const yep_dnode* n) {
275
307
  }
276
308
  }
277
309
 
278
- static void emit_scalar(yep_writer* w, const yep_dnode* n, int parent_col, int as_key) {
279
- yep_view vdec = wv(w, n->value);
280
- const char* p = (const char*)vdec.p;
281
- uint32_t len = vdec.len;
282
- if (w->canonical || w->json) {
283
- emit_canonical_scalar(w, n);
284
- return;
285
- }
286
- /* p may be NULL for an empty view; memchr(NULL, .., 0) is UB by
287
- * the nonnull attribute even though it reads nothing */
310
+ /* The fidelity-mode emission route for one scalar (#352): the dry
311
+ * pass derives it exactly once; the wet pass consumes the recorded
312
+ * byte and skips the multiline/blockable/safety analysis entirely. */
313
+ enum { YEP_SC_NOTHING = 0, YEP_SC_PLAIN, YEP_SC_SQ, YEP_SC_DQ, YEP_SC_LITERAL };
314
+ #define YEP_SC_MEMO_MIN 32u /* bytes; derive-below, memoize-above */
315
+
316
+ static uint8_t sc_route(yep_writer* w, const char* p, uint32_t len, int as_key, uint8_t sty_in) {
288
317
  int multiline = (len > 0 && memchr(p, '\n', len) != NULL);
289
318
  int blockable = 0;
290
319
  if (multiline && !as_key) {
291
- /* A block whose body is all whitespace parses back empty, and a
292
- * raw CR re-parses as a break — both emit double-quoted. */
293
320
  int allws = 1;
294
321
  int has_cr = 0;
295
322
  for (uint32_t i = 0; i < len; i++) {
@@ -302,68 +329,88 @@ static void emit_scalar(yep_writer* w, const yep_dnode* n, int parent_col, int a
302
329
  }
303
330
  blockable = !allws && !has_cr;
304
331
  }
305
- int sty = n->style;
332
+ uint8_t sty = sty_in;
306
333
  if (sty == 4 || sty == 5) {
307
334
  if (multiline && as_key) {
308
- emit_dq(w, p, len); /* keys stay inline: no block scalars */
309
- return;
335
+ return YEP_SC_DQ;
310
336
  }
311
337
  if (multiline && blockable) {
312
- emit_literal(w, n, parent_col);
313
- return;
338
+ return YEP_SC_LITERAL;
314
339
  }
315
340
  if (multiline) {
316
- emit_dq(w, p, len);
317
- return;
341
+ return YEP_SC_DQ;
318
342
  }
319
343
  if (len == 0) {
320
- emit_dq(w, p, len); /* an explicitly-empty block scalar is
321
- the empty STRING — a bare empty would
322
- re-read as null (2G84#3, K858) */
323
- return;
344
+ return YEP_SC_DQ;
324
345
  }
325
- sty = 1; /* a chomp-stripped block is plain bytes: re-emit plain */
346
+ sty = 1;
326
347
  }
327
348
  switch (sty) {
328
- case 1: /* plain */
349
+ case 1:
329
350
  if (len == 0) {
330
- if (as_key) {
331
- emit_dq(w, p, len); /* empty keys must stay visible */
332
- }
333
- break; /* an empty plain value emits as nothing (libyaml's
334
- null rendering, issue #290) */
351
+ return as_key ? YEP_SC_DQ : YEP_SC_NOTHING;
335
352
  }
336
- if ((as_key ? yep_style_plain_key_safe(p, len) : yep_style_plain_safe(p, len))) {
337
- wr_put(w, p, len);
338
- } else if (!multiline) {
339
- /* libyaml parity: a scalar that must be quoted but needs no
340
- * escapes (digit-leading strings like "5014", indicator
341
- * leads) single-quotes — double quotes are for text that
342
- * actually requires escapes (issue #95's matrix) */
343
- int clean = 1;
353
+ if (as_key ? yep_style_plain_key_safe(p, len) : yep_style_plain_safe(p, len)) {
354
+ return YEP_SC_PLAIN;
355
+ }
356
+ if (!multiline) {
344
357
  for (uint32_t i = 0; i < len; i++) {
345
358
  if ((unsigned char)p[i] < 0x20 || (unsigned char)p[i] == 0x7f) {
346
- clean = 0;
347
- break;
359
+ return YEP_SC_DQ;
348
360
  }
349
361
  }
350
- if (clean) {
351
- emit_sq(w, p, len);
352
- } else {
353
- emit_dq(w, p, len);
354
- }
355
- } else {
356
- emit_dq(w, p, len);
362
+ return YEP_SC_SQ;
357
363
  }
358
- break;
359
- case 2: /* single */
360
- if (!multiline) {
361
- emit_sq(w, p, len);
364
+ return YEP_SC_DQ;
365
+ case 2:
366
+ return multiline ? YEP_SC_DQ : YEP_SC_SQ;
367
+ default:
368
+ return YEP_SC_DQ;
369
+ }
370
+ }
371
+
372
+ static void emit_scalar(yep_writer* w, const yep_dnode* n, int parent_col, int as_key) {
373
+ yep_view vdec = wv(w, n->value);
374
+ const char* p = (const char*)vdec.p;
375
+ uint32_t len = vdec.len;
376
+ if (w->canonical || w->json) {
377
+ emit_canonical_scalar(w, n);
378
+ return;
379
+ }
380
+ (void)0;
381
+ uint8_t route;
382
+ /* The memo pays only for LONG scalars: the derive cost scales with
383
+ * len (safety walk, quote scans) while the memo is a fixed store +
384
+ * cold load — on the short scalars of table-shaped documents the
385
+ * memo measured SLOWER than re-deriving (the #357 first artifact:
386
+ * json-users -36%, scalar-heavy +24%). Below the threshold both
387
+ * passes derive; the consumption-order index covers only the
388
+ * memoized long scalars, in visit order either way. */
389
+ if (w->sc_dec != NULL && len >= YEP_SC_MEMO_MIN) {
390
+ if (w->dry) {
391
+ route = sc_route(w, p, len, as_key, n->style);
392
+ w->sc_dec[w->sc_i] = route;
362
393
  } else {
363
- emit_dq(w, p, len);
394
+ route = w->sc_dec[w->sc_i];
364
395
  }
396
+ w->sc_i++;
397
+ } else {
398
+ route = sc_route(w, p, len, as_key, n->style);
399
+ }
400
+ switch (route) {
401
+ case YEP_SC_NOTHING:
402
+ break; /* an empty plain value emits as nothing (libyaml's null
403
+ rendering, issue #290) */
404
+ case YEP_SC_PLAIN:
405
+ wr_put(w, p, len);
406
+ break;
407
+ case YEP_SC_SQ:
408
+ emit_sq(w, p, len);
409
+ break;
410
+ case YEP_SC_LITERAL:
411
+ emit_literal(w, n, parent_col);
365
412
  break;
366
- default: /* double, literal-as-key, folded-as-key, any */
413
+ default:
367
414
  emit_dq(w, p, len);
368
415
  break;
369
416
  }
@@ -694,6 +741,7 @@ size_t yep_emit_run(yep_emitter* em, int dry) {
694
741
  w->flushed = 0;
695
742
  w->sink_aborted = 0;
696
743
  w->col = 0;
744
+ w->sc_i = 0;
697
745
  yep_nametab_clear(&em->canon_names);
698
746
  if (w->json) {
699
747
  /* JSON has no multi-document streams and no document marker */
@@ -40,6 +40,21 @@ typedef struct yep_writer {
40
40
  int col; /* current output column (wr_* maintains) */
41
41
  const char* sv_input; /* compact-view decode bases (dom regions) */
42
42
  const char* sv_arena;
43
+ /* #352: per-scalar route decisions, derived once in the dry
44
+ * sizing pass and consumed by the wet write pass (one byte per
45
+ * scalar, indexed by consumption order; NULL = derive per pass) */
46
+ uint8_t* sc_dec;
47
+ uint32_t sc_i;
48
+ /* #352 slice 2: the growable single-pass route — the exact-sizing
49
+ * dry pass measured 0.54-0.73x of the one-pass stream route on
50
+ * CI-fresh runners, so serialize_ex writes wet into a buffer that
51
+ * doubles on demand (cap/grow), like the streaming window. The
52
+ * fixed-buffer routes keep grow=0 and today's trust-the-count
53
+ * behavior. oom marks a failed growth: writes stop touching memory
54
+ * but the byte count keeps running so callers can fail cleanly. */
55
+ size_t cap;
56
+ int grow;
57
+ int oom;
43
58
  } yep_writer;
44
59
 
45
60
  typedef struct yep_emitter {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.10.1
4
+ version: 0.6.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-19 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi