yeptris 0.6.33.1 → 0.6.34.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: a8b675b39f6785429f94719e6181385b944307a0d0ffa4ce110e632ff6a32376
4
- data.tar.gz: e9adc21e40e03957bbabc17b6a05ff14b45407d8e664cf4010f40a5696cb2369
3
+ metadata.gz: f0e9eb62d76c5029f2712f49649f6b8fe366a5a2c78c4d1360148ad9afa101c5
4
+ data.tar.gz: 81495275aea44c42938fdcb2fbbe3b693d20fc7cae24d0e575fa807436c1f870
5
5
  SHA512:
6
- metadata.gz: 3321c98a84e187037ccf67af14fef2a4ea0ec2570c2d5f9ffb962ba9199959a8d1cefdbf146963cc208536cbfc575c012078548226456f3ac7f485f91a44f897
7
- data.tar.gz: 255ae290f8227e62f026629a5ee69666535c0442fa167877223cbc0cbe2d9ce74398b98c75f706a370765714fe5694fc2b3b165f0539a7ffd8c5e931080092de
6
+ metadata.gz: 23b3e575e2cf67681d6c1d02d93d3fdc810c1f2233f80d9e81e32870fcd9c3ee204326fe8546d35dc5f1b674b8fd98a8bc1fb42e5a7a90dfe9c62f2e82b21981
7
+ data.tar.gz: e936ed4c8980fbe336cafb2bb7c60024099695e942adc7ccb967384b3a947f70d9f895a26ee853adecd0c1bb2deb558db3f82468a6d2adf8a1b494ca82eb5e35
data/README.adoc CHANGED
@@ -5,6 +5,15 @@ https://github.com/leptris/yeptris[libyeptris] — the YAML counterpart
5
5
  of libleptris. Psych-compatible semantics, one shared library, zero
6
6
  compilation at install.
7
7
 
8
+ Part of the yeptris program: *the best (multiple dimensions)
9
+ YAML/JSON parser/builder ever* — conformance, performance,
10
+ safety/hardening, API surfaces, transport correctness, and trust,
11
+ held simultaneously, in C, Ruby, Rust, and JS. This binding is the
12
+ most complete surface: the Psych drop-in namespace, JSON.parse
13
+ parity with the strict duplicate-key contract, CBOR, and ValueML,
14
+ tracking every libyeptris release in lockstep. The achievement
15
+ program lives in the engine's PLAN.md ("The best-ever program").
16
+
8
17
  == Install (development)
9
18
 
10
19
  The gem loads a shared `libyeptris` — from `YEPTRIS_LIB_PATH`, a
@@ -173,9 +173,19 @@ static VALUE jr_object(jr* j) {
173
173
  }
174
174
 
175
175
  static VALUE jr_object_body(jr* j, VALUE h_pre) {
176
+ /* The pairs staging buffer MUST live in GC-scanned memory: pairs
177
+ * beyond the current one exist only there once the key/val locals
178
+ * are reused, so a malloc'd buffer makes them invisible to the
179
+ * collector — it can free them mid-parse and recycle their slots,
180
+ * and rb_hash_bulk_insert then inserts garbage (yeptris-ruby#279:
181
+ * the arm64-darwin SIGSEGV whose fault address was document text).
182
+ * The stack array IS scanned, so the bulk fast path is bounded at
183
+ * 31 pairs; beyond that the buffered pairs flush with one
184
+ * bulk_insert and the rest of the object rides rb_hash_aset. */
176
185
  VALUE pairs[64];
177
186
  VALUE* pv = pairs;
178
187
  size_t pcap = 64, pn = 0, heap_cap = 0;
188
+ int aset_rest = 0;
179
189
  for (;;) {
180
190
  jr_ws(j);
181
191
  if (j->i >= j->len || j->p[j->i] != '"') { j->err = -2; goto out; }
@@ -186,7 +196,7 @@ static VALUE jr_object_body(jr* j, VALUE h_pre) {
186
196
  j->i++;
187
197
  VALUE val = jr_value(j);
188
198
  if (j->err) goto out;
189
- if (yep_ins_mode == YEP_INS_ASET || j->strict_dup) {
199
+ if (yep_ins_mode == YEP_INS_ASET || j->strict_dup || aset_rest) {
190
200
  VALUE h = h_pre == Qundef ? (h_pre = HASH_NEW_CAPA(8)) : h_pre;
191
201
  if (j->strict_dup && !NIL_P(rb_hash_aref(h, key))) {
192
202
  j->err = -3;
@@ -196,15 +206,21 @@ static VALUE jr_object_body(jr* j, VALUE h_pre) {
196
206
  rb_hash_aset(h, key, val);
197
207
  } else {
198
208
  if (pn + 2 > pcap) {
199
- size_t ncap = pcap * 2;
200
- VALUE* nv = malloc(ncap * sizeof(VALUE));
201
- if (!nv) { j->err = -1; goto out; }
202
- memcpy(nv, pv, pn * sizeof(VALUE));
203
- if (pv != pairs) { free(pv); heap_cap = 1; }
204
- pv = nv; pcap = ncap;
209
+ /* buffer full: flush what is staged (no heap staging —
210
+ * see the #279 note above); THIS pair and the rest of
211
+ * the object ride aset — no continue here, the
212
+ * delimiter scan below must still run */
213
+ VALUE h = (h_pre == Qundef) ? (h_pre = HASH_NEW_CAPA((long)(pn / 2) + 8)) : h_pre;
214
+ rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
215
+ pn = 0;
216
+ aset_rest = 1;
217
+ }
218
+ if (aset_rest) {
219
+ rb_hash_aset(h_pre, key, val);
220
+ } else {
221
+ pv[pn++] = key;
222
+ pv[pn++] = val;
205
223
  }
206
- pv[pn++] = key;
207
- pv[pn++] = val;
208
224
  }
209
225
  jr_ws(j);
210
226
  if (j->i >= j->len) { j->err = -2; goto out; }
@@ -351,7 +367,17 @@ double yep_rb_scan_time(const char* p, size_t len, int n) {
351
367
  VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
352
368
  jr j;
353
369
  memset(&j, 0, sizeof(j));
354
- j.p = p; j.len = len; j.enc = rb_utf8_encoding();
370
+ /* The parse holds j.p across unbounded allocations, and the GC
371
+ * strategy knobs leave collection ENABLED during the window
372
+ * (arm64's default is none). A compaction moving the source
373
+ * string would dangle every j->p read after the move
374
+ * (yeptris-ruby#279's heap-layout-dependent SIGSEGV class), so
375
+ * the parse runs on a private copy: one memcpy of the input,
376
+ * then no Ruby object backs the cursor. */
377
+ char* own = malloc(len ? len : 1);
378
+ if (own == NULL) rb_raise(rb_eNoMemError, "yeptris native json");
379
+ memcpy(own, p, len);
380
+ j.p = own; j.len = len; j.enc = rb_utf8_encoding();
355
381
  j.strict_dup = strict_dup;
356
382
  VALUE already = Qtrue;
357
383
  if (yep_gc_mode != YEP_GC_NONE) already = rb_gc_disable();
@@ -359,6 +385,7 @@ VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
359
385
  jr_ws(&j);
360
386
  if (j.i != len && j.err == 0) j.err = -2;
361
387
  free(j.scratch);
388
+ free(own);
362
389
  if (already == Qfalse) rb_gc_enable();
363
390
  if (yep_gc_mode == YEP_GC_START && j.err == 0) rb_gc_start();
364
391
  if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
@@ -366,6 +393,11 @@ VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
366
393
  rb_raise(rb_path2class("Yeptris::ParseError"), "duplicate key \"%s\" in JSON object",
367
394
  RSTRING_PTR(j.dup_key));
368
395
  }
369
- if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
396
+ if (j.err != 0) {
397
+ /* the tape engine's wording ("not strict JSON at byte N") so
398
+ * consumers can pin one error shape across both routes
399
+ * (yeptris-ruby#279's adjacent note) */
400
+ rb_raise(rb_path2class("Yeptris::ParseError"), "not strict JSON at byte %zu", j.i);
401
+ }
370
402
  return v;
371
403
  }
data/lib/yeptris/ffi.rb CHANGED
@@ -228,12 +228,17 @@ module Yeptris
228
228
  %i[pointer size_t int int pointer pointer], :int
229
229
  attach_function :yeptris_marshal_node,
230
230
  %i[yeptris_node pointer pointer], :int
231
+ attach_function :yeptris_marshal_node_ex,
232
+ %i[yeptris_node uint32 pointer pointer], :int
231
233
  attach_function :yeptris_marshal_free, [:pointer], :void
232
234
  # The bulk child drain (#168's quadratic): O(n) iteration for
233
235
  # #each/#each_pair. Engines without it fall back to the per-index
234
236
  # walk (correct, quadratic).
235
237
  CHILDREN_DRAIN = !(@missing ||= []).include?(:yeptris_node_children)
236
238
  MARSHAL = !(@missing ||= []).include?(:yeptris_marshal_node)
239
+ # the strict-dup marshal entry (engine 0.6.34+, yeptris-ruby#278):
240
+ # gates the JSON fallback's C route in STRICT mode
241
+ MARSHAL_STRICT_DUP = !(@missing ||= []).include?(:yeptris_marshal_node_ex)
237
242
 
238
243
  # bulk build (TODO.impl/15 phase D): one call raises a document
239
244
  BUILD_SCALAR = 1
data/lib/yeptris/json.rb CHANGED
@@ -9,10 +9,13 @@ module Yeptris
9
9
  # keeps the Psych contract for every input (JSON-shaped included),
10
10
  # so the two never drift into each other's semantics.
11
11
  #
12
- # Engines, fastest first (both exact — the parity spec runs against
12
+ # Engines, fastest first (all exact — the parity spec runs against
13
13
  # whichever is loaded):
14
14
  # 1. the native materializer (fused C scan → VALUE; opt-in build)
15
- # 2. the record drain + strict conversion walk (always available)
15
+ # 2. the marshal engine (#278): strict-JSON parse + Marshal 4.8
16
+ # emission + ONE core Marshal.load — all C, no per-value Ruby
17
+ # (engine 0.6.34+; the strict flag follows the resolved json gem)
18
+ # 3. the record drain + strict conversion walk (always available)
16
19
  module JSON
17
20
  class Error < ::Yeptris::Error; end
18
21
  class ParseError < Error; end
@@ -39,9 +42,46 @@ module Yeptris
39
42
  raise ParseError, e.message
40
43
  end
41
44
  end
45
+ # yeptris-ruby#278: with no Ruby-version-matched native build the
46
+ # pure-Ruby walk measured 0.11x vs JSON.parse on real documents —
47
+ # the marshal engine keeps the fallback C-side end to end.
48
+ return marshal_engine(source, 0) if ::Yeptris::FFI::MARSHAL_STRICT_DUP
42
49
  tape_engine(source)
43
50
  end
44
51
 
52
+ # The marshal engine (#278): yeptris_parse_json gates the strict
53
+ # grammar, yeptris_marshal_node_ex writes the Ruby Marshal 4.8
54
+ # stream (STRICT_DUP_KEYS raising on json 3's default contract),
55
+ # and ONE core Marshal.load materializes the graph. Returns nil
56
+ # when the engine cannot serve the route (caller keeps walking);
57
+ # every parse/dup failure raises ParseError.
58
+ def marshal_engine(source, flags)
59
+ begin
60
+ gate = ::Yeptris::Document.parse_json(source)
61
+ rescue ::Yeptris::ParseError => e
62
+ raise ParseError, e.message
63
+ end
64
+ begin
65
+ out_p = ::FFI::MemoryPointer.new(:pointer)
66
+ olen_p = ::FFI::MemoryPointer.new(:size_t)
67
+ st = ::Yeptris::FFI.yeptris_marshal_node_ex(
68
+ ::Yeptris::FFI.yeptris_document_root(gate.c_ptr, 0), flags, out_p, olen_p
69
+ )
70
+ if st != ::Yeptris::FFI::OK
71
+ raise ParseError, ::Yeptris::FFI.last_error_message if st == ::Yeptris::FFI::ERROR_PARSE
72
+
73
+ return nil # UNSUPPORTED: merge keys et al. cannot occur in JSON
74
+ end
75
+ buf = out_p.read_pointer
76
+ data = buf.read_bytes(::Yeptris::FFI.read_c_size_t(olen_p))
77
+ data.force_encoding(Encoding::ASCII_8BIT)
78
+ ::Yeptris::FFI.yeptris_marshal_free(buf)
79
+ Marshal.load(data)
80
+ ensure
81
+ gate.free
82
+ end
83
+ end
84
+
45
85
  # The tape engine (issue #81): ONE call validates and materializes
46
86
  # the record stream; the walk reads four bulk columns and slices
47
87
  # strings straight out of the caller's source — no gate document,
@@ -212,6 +252,12 @@ module Yeptris
212
252
  raise ParseError, e.message
213
253
  end
214
254
  begin
255
+ if ::Yeptris::FFI::MARSHAL_STRICT_DUP
256
+ routed = marshal_engine(
257
+ source, STRICT_DUPLICATE_KEYS ? ::Yeptris::FFI::MARSHAL_STRICT_DUP_KEYS : 0
258
+ )
259
+ return routed unless routed.nil?
260
+ end
215
261
  cols = ::Yeptris::FFI::ValueColumns.new
216
262
  st = ::Yeptris::FFI.yeptris_value_drain_columns(
217
263
  source, source.bytesize, ::Yeptris::FFI::SCHEMA_12_CORE, cols
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.33.1".freeze
6
+ VERSION = "0.6.34.1".freeze
7
7
 
8
8
  # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
9
  # embed yeptris inside CLIs that assert clean stderr, and a
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.33
6
+ VERSION 0.6.34
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -54,6 +54,7 @@ set(YEPTRIS_SOURCES
54
54
  yeptris/emit/emit.c
55
55
  yeptris/emit/float/print.c
56
56
  yeptris/emit/float/dragon.c
57
+ yeptris/emit/float/fpconv.c
57
58
  )
58
59
 
59
60
  if(YEPTRIS_WITH_CBOR)
@@ -43,6 +43,17 @@ YEPTRIS_API YeptrisStatus yeptris_marshal(const char* data, size_t len, YeptrisS
43
43
  * bulk path behind Node#to_ruby (one call, not one per node). */
44
44
  YEPTRIS_API YeptrisStatus yeptris_marshal_node(YeptrisNode node, char** out, size_t* out_len);
45
45
 
46
+ #define YEPTRIS_MARSHAL_STRICT_DUP_KEYS 1u
47
+
48
+ /* yeptris_marshal_node with flags. STRICT_DUP_KEYS raises
49
+ * YEPTRIS_ERROR_PARSE ("duplicate key in object") when any map carries
50
+ * the same key twice — json 3's default contract; the JSON DOM keeps
51
+ * both pairs, and the non-strict stream stays last-wins (json 2.x).
52
+ * Memory: *out is malloc'd, free with yeptris_marshal_free exactly
53
+ * once; on failure nothing is allocated. */
54
+ YEPTRIS_API YeptrisStatus yeptris_marshal_node_ex(YeptrisNode node, uint32_t flags, char** out,
55
+ size_t* out_len);
56
+
46
57
  YEPTRIS_API void yeptris_marshal_free(char* out);
47
58
 
48
59
  #ifdef __cplusplus
@@ -31,6 +31,11 @@ int yep_d2fixed(double d, uint32_t precision, char* buf);
31
31
  /* YAML words for non-finite values (finite input returns 0). */
32
32
  int yep_d2s_nonfinite(double d, char* buf);
33
33
 
34
+ /* The json-gem float byte shape (grisu2 digits + fpconv emit
35
+ * windows, vendored in fpconv.c) for JSON-mode canonical scalars
36
+ * (yeptris#651). 32-byte buffer. Returns the length written. */
37
+ int yep_d2s_json(double d, char* buf);
38
+
34
39
  #ifdef __cplusplus
35
40
  }
36
41
  #endif
@@ -0,0 +1,428 @@
1
+ /* fpconv.c — the json-gem float byte shape (yeptris#651).
2
+ *
3
+ * Vendored from night-shift/fpconv in the variant carried by the Ruby
4
+ * json package (ruby/json ext/json/ext/vendor/fpconv.c), which appends
5
+ * ".0" to integral floats. JSON.generate on json >= 2.10 emits floats
6
+ * through exactly this code, so byte parity for the JSON writer
7
+ * (emit_canonical_scalar's JSON route) requires the same digits AND
8
+ * the same emit windows: grisu2 is not always-shortest, so the
9
+ * clean-room shortest printer cannot be reshaped to match.
10
+ *
11
+ * Boost Software License - Version 1.0 - August 17th, 2003.
12
+ * Permission is hereby granted, free of charge, to any person or
13
+ * organization obtaining a copy of this software (see
14
+ * https://www.boost.org/LICENSE_1_0.txt). Attribution preserved per
15
+ * the vendoring discipline (cf. the ryu port plan).
16
+ */
17
+
18
+ #include <stdbool.h>
19
+ #include <stdint.h>
20
+ #include <string.h>
21
+
22
+ #include "emit/float/api.h"
23
+
24
+ #define npowers 87
25
+ #define steppowers 8
26
+ #define firstpower -348 /* 10 ^ -348 */
27
+
28
+ #define expmax -32
29
+ #define expmin -60
30
+
31
+ typedef struct Fp {
32
+ uint64_t frac;
33
+ int exp;
34
+ } Fp;
35
+
36
+ static const Fp powers_ten[] = {
37
+ {18054884314459144840U, -1220}, {13451937075301367670U, -1193}, {10022474136428063862U, -1166},
38
+ {14934650266808366570U, -1140}, {11127181549972568877U, -1113}, {16580792590934885855U, -1087},
39
+ {12353653155963782858U, -1060}, {18408377700990114895U, -1034}, {13715310171984221708U, -1007},
40
+ {10218702384817765436U, -980}, {15227053142812498563U, -954}, {11345038669416679861U, -927},
41
+ {16905424996341287883U, -901}, {12595523146049147757U, -874}, {9384396036005875287U, -847},
42
+ {13983839803942852151U, -821}, {10418772551374772303U, -794}, {15525180923007089351U, -768},
43
+ {11567161174868858868U, -741}, {17236413322193710309U, -715}, {12842128665889583758U, -688},
44
+ {9568131466127621947U, -661}, {14257626930069360058U, -635}, {10622759856335341974U, -608},
45
+ {15829145694278690180U, -582}, {11793632577567316726U, -555}, {17573882009934360870U, -529},
46
+ {13093562431584567480U, -502}, {9755464219737475723U, -475}, {14536774485912137811U, -449},
47
+ {10830740992659433045U, -422}, {16139061738043178685U, -396}, {12024538023802026127U, -369},
48
+ {17917957937422433684U, -343}, {13349918974505688015U, -316}, {9946464728195732843U, -289},
49
+ {14821387422376473014U, -263}, {11042794154864902060U, -236}, {16455045573212060422U, -210},
50
+ {12259964326927110867U, -183}, {18268770466636286478U, -157}, {13611294676837538539U, -130},
51
+ {10141204801825835212U, -103}, {15111572745182864684U, -77}, {11258999068426240000U, -50},
52
+ {16777216000000000000U, -24}, {12500000000000000000U, 3}, {9313225746154785156U, 30},
53
+ {13877787807814456755U, 56}, {10339757656912845936U, 83}, {15407439555097886824U, 109},
54
+ {11479437019748901445U, 136}, {17105694144590052135U, 162}, {12744735289059618216U, 189},
55
+ {9495567745759798747U, 216}, {14149498560666738074U, 242}, {10542197943230523224U, 269},
56
+ {15709099088952724970U, 295}, {11704190886730495818U, 322}, {17440603504673385349U, 348},
57
+ {12994262207056124023U, 375}, {9681479787123295682U, 402}, {14426529090290212157U, 428},
58
+ {10748601772107342003U, 455}, {16016664761464807395U, 481}, {11933345169920330789U, 508},
59
+ {17782069995880619868U, 534}, {13248674568444952270U, 561}, {9871031767461413346U, 588},
60
+ {14708983551653345445U, 614}, {10959046745042015199U, 641}, {16330252207878254650U, 667},
61
+ {12166986024289022870U, 694}, {18130221999122236476U, 720}, {13508068024458167312U, 747},
62
+ {10064294952495520794U, 774}, {14996968138956309548U, 800}, {11173611982879273257U, 827},
63
+ {16649979327439178909U, 853}, {12405201291620119593U, 880}, {9242595204427927429U, 907},
64
+ {13772540099066387757U, 933}, {10261342003245940623U, 960}, {15290591125556738113U, 986},
65
+ {11392378155556871081U, 1013}, {16975966327722178521U, 1039}, {12648080533535911531U, 1066}};
66
+
67
+ static Fp find_cachedpow10(int exp, int* k) {
68
+ const double one_log_ten = 0.30102999566398114;
69
+
70
+ int approx = (int)(-(exp + npowers) * one_log_ten);
71
+ int idx = (approx - firstpower) / steppowers;
72
+
73
+ while (1) {
74
+ int current = exp + powers_ten[idx].exp + 64;
75
+
76
+ if (current < expmin) {
77
+ idx++;
78
+ continue;
79
+ }
80
+
81
+ if (current > expmax) {
82
+ idx--;
83
+ continue;
84
+ }
85
+
86
+ *k = (firstpower + idx * steppowers);
87
+
88
+ return powers_ten[idx];
89
+ }
90
+ }
91
+
92
+ #define fracmask 0x000FFFFFFFFFFFFFU
93
+ #define expmask 0x7FF0000000000000U
94
+ #define hiddenbit 0x0010000000000000U
95
+ #define signmask 0x8000000000000000U
96
+ #define expbias (1023 + 52)
97
+
98
+ #define absv(n) ((n) < 0 ? -(n) : (n))
99
+ #define minv(a, b) ((a) < (b) ? (a) : (b))
100
+
101
+ static const uint64_t tens[] = {10000000000000000000U,
102
+ 1000000000000000000U,
103
+ 100000000000000000U,
104
+ 10000000000000000U,
105
+ 1000000000000000U,
106
+ 100000000000000U,
107
+ 10000000000000U,
108
+ 1000000000000U,
109
+ 100000000000U,
110
+ 10000000000U,
111
+ 1000000000U,
112
+ 100000000U,
113
+ 10000000U,
114
+ 1000000U,
115
+ 100000U,
116
+ 10000U,
117
+ 1000U,
118
+ 100U,
119
+ 10U,
120
+ 1U};
121
+
122
+ static uint64_t get_dbits(double d) {
123
+ union {
124
+ double dbl;
125
+ uint64_t i;
126
+ } dbl_bits = {d};
127
+
128
+ return dbl_bits.i;
129
+ }
130
+
131
+ static Fp build_fp(double d) {
132
+ uint64_t bits = get_dbits(d);
133
+
134
+ Fp fp;
135
+ fp.frac = bits & fracmask;
136
+ fp.exp = (bits & expmask) >> 52;
137
+
138
+ if (fp.exp) {
139
+ fp.frac += hiddenbit;
140
+ fp.exp -= expbias;
141
+
142
+ } else {
143
+ fp.exp = -expbias + 1;
144
+ }
145
+
146
+ return fp;
147
+ }
148
+
149
+ static void normalize(Fp* fp) {
150
+ while ((fp->frac & hiddenbit) == 0) {
151
+ fp->frac <<= 1;
152
+ fp->exp--;
153
+ }
154
+
155
+ int shift = 64 - 52 - 1;
156
+ fp->frac <<= shift;
157
+ fp->exp -= shift;
158
+ }
159
+
160
+ static void get_normalized_boundaries(Fp* fp, Fp* lower, Fp* upper) {
161
+ upper->frac = (fp->frac << 1) + 1;
162
+ upper->exp = fp->exp - 1;
163
+
164
+ while ((upper->frac & (hiddenbit << 1)) == 0) {
165
+ upper->frac <<= 1;
166
+ upper->exp--;
167
+ }
168
+
169
+ int u_shift = 64 - 52 - 2;
170
+
171
+ upper->frac <<= u_shift;
172
+ upper->exp = upper->exp - u_shift;
173
+
174
+ int l_shift = fp->frac == hiddenbit ? 2 : 1;
175
+
176
+ lower->frac = (fp->frac << l_shift) - 1;
177
+ lower->exp = fp->exp - l_shift;
178
+
179
+ lower->frac <<= lower->exp - upper->exp;
180
+ lower->exp = upper->exp;
181
+ }
182
+
183
+ static Fp multiply(Fp* a, Fp* b) {
184
+ const uint64_t lomask = 0x00000000FFFFFFFF;
185
+
186
+ uint64_t ah_bl = (a->frac >> 32) * (b->frac & lomask);
187
+ uint64_t al_bh = (a->frac & lomask) * (b->frac >> 32);
188
+ uint64_t al_bl = (a->frac & lomask) * (b->frac & lomask);
189
+ uint64_t ah_bh = (a->frac >> 32) * (b->frac >> 32);
190
+
191
+ uint64_t tmp = (ah_bl & lomask) + (al_bh & lomask) + (al_bl >> 32);
192
+ /* round up */
193
+ tmp += 1U << 31;
194
+
195
+ Fp fp = {ah_bh + (ah_bl >> 32) + (al_bh >> 32) + (tmp >> 32), a->exp + b->exp + 64};
196
+
197
+ return fp;
198
+ }
199
+
200
+ static void round_digit(char* digits, int ndigits, uint64_t delta, uint64_t rem, uint64_t kappa,
201
+ uint64_t frac) {
202
+ while (rem < frac && delta - rem >= kappa &&
203
+ (rem + kappa < frac || frac - rem > rem + kappa - frac)) {
204
+ digits[ndigits - 1]--;
205
+ rem += kappa;
206
+ }
207
+ }
208
+
209
+ static int generate_digits(Fp* fp, Fp* upper, Fp* lower, char* digits, int* K) {
210
+ uint64_t wfrac = upper->frac - fp->frac;
211
+ uint64_t delta = upper->frac - lower->frac;
212
+
213
+ Fp one;
214
+ one.frac = 1ULL << -upper->exp;
215
+ one.exp = upper->exp;
216
+
217
+ uint64_t part1 = upper->frac >> -one.exp;
218
+ uint64_t part2 = upper->frac & (one.frac - 1);
219
+
220
+ int idx = 0, kappa = 10;
221
+ const uint64_t* divp;
222
+ /* 1000000000 */
223
+ for (divp = tens + 10; kappa > 0; divp++) {
224
+ uint64_t div = *divp;
225
+ unsigned digit = (unsigned)(part1 / div);
226
+
227
+ if (digit || idx) {
228
+ digits[idx++] = (char)(digit + '0');
229
+ }
230
+
231
+ part1 -= digit * div;
232
+ kappa--;
233
+
234
+ uint64_t tmp = (part1 << -one.exp) + part2;
235
+ if (tmp <= delta) {
236
+ *K += kappa;
237
+ round_digit(digits, idx, delta, tmp, div << -one.exp, wfrac);
238
+
239
+ return idx;
240
+ }
241
+ }
242
+
243
+ /* 10 */
244
+ const uint64_t* unit = tens + 18;
245
+
246
+ while (true) {
247
+ part2 *= 10;
248
+ delta *= 10;
249
+ kappa--;
250
+
251
+ unsigned digit = (unsigned)(part2 >> -one.exp);
252
+ if (digit || idx) {
253
+ digits[idx++] = (char)(digit + '0');
254
+ }
255
+
256
+ part2 &= one.frac - 1;
257
+ if (part2 < delta) {
258
+ *K += kappa;
259
+ round_digit(digits, idx, delta, part2, one.frac, wfrac * *unit);
260
+
261
+ return idx;
262
+ }
263
+
264
+ unit--;
265
+ }
266
+ }
267
+
268
+ static int grisu2(double d, char* digits, int* K) {
269
+ Fp w = build_fp(d);
270
+
271
+ Fp lower, upper;
272
+ get_normalized_boundaries(&w, &lower, &upper);
273
+
274
+ normalize(&w);
275
+
276
+ int k;
277
+ Fp cp = find_cachedpow10(upper.exp, &k);
278
+
279
+ w = multiply(&w, &cp);
280
+ upper = multiply(&upper, &cp);
281
+ lower = multiply(&lower, &cp);
282
+
283
+ lower.frac++;
284
+ upper.frac--;
285
+
286
+ *K = -k;
287
+
288
+ return generate_digits(&w, &upper, &lower, digits, K);
289
+ }
290
+
291
+ static int emit_digits(char* digits, int ndigits, char* dest, int K, bool neg) {
292
+ int exp = absv(K + ndigits - 1);
293
+
294
+ if (K >= 0 && exp < 15) {
295
+ memcpy(dest, digits, (size_t)ndigits);
296
+ memset(dest + ndigits, '0', (size_t)K);
297
+
298
+ /* add a .0 to mark this as a float. */
299
+ dest[ndigits + K] = '.';
300
+ dest[ndigits + K + 1] = '0';
301
+
302
+ return ndigits + K + 2;
303
+ }
304
+
305
+ /* write decimal w/o scientific notation */
306
+ if (K < 0 && (K > -7 || exp < 10)) {
307
+ int offset = ndigits - absv(K);
308
+ /* fp < 1.0 -> write leading zero */
309
+ if (offset <= 0) {
310
+ offset = -offset;
311
+ dest[0] = '0';
312
+ dest[1] = '.';
313
+ memset(dest + 2, '0', (size_t)offset);
314
+ memcpy(dest + offset + 2, digits, (size_t)ndigits);
315
+
316
+ return ndigits + 2 + offset;
317
+
318
+ /* fp > 1.0 */
319
+ } else {
320
+ memcpy(dest, digits, (size_t)offset);
321
+ dest[offset] = '.';
322
+ memcpy(dest + offset + 1, digits + offset, (size_t)(ndigits - offset));
323
+
324
+ return ndigits + 1;
325
+ }
326
+ }
327
+
328
+ /* write decimal w/ scientific notation */
329
+ ndigits = minv(ndigits, 18 - neg);
330
+
331
+ int idx = 0;
332
+ dest[idx++] = digits[0];
333
+
334
+ if (ndigits > 1) {
335
+ dest[idx++] = '.';
336
+ memcpy(dest + idx, digits + 1, (size_t)(ndigits - 1));
337
+ idx += ndigits - 1;
338
+ }
339
+
340
+ dest[idx++] = 'e';
341
+
342
+ char sign = K + ndigits - 1 < 0 ? '-' : '+';
343
+ dest[idx++] = sign;
344
+
345
+ int cent = 0;
346
+
347
+ if (exp > 99) {
348
+ cent = exp / 100;
349
+ dest[idx++] = (char)(cent + '0');
350
+ exp -= cent * 100;
351
+ }
352
+ if (exp > 9) {
353
+ int dec = exp / 10;
354
+ dest[idx++] = (char)(dec + '0');
355
+ exp -= dec * 10;
356
+
357
+ } else if (cent) {
358
+ dest[idx++] = '0';
359
+ }
360
+
361
+ dest[idx++] = (char)(exp % 10 + '0');
362
+
363
+ return idx;
364
+ }
365
+
366
+ static int filter_special(double fp, char* dest) {
367
+ if (fp == 0.0) {
368
+ dest[0] = '0';
369
+ dest[1] = '.';
370
+ dest[2] = '0';
371
+ return 3;
372
+ }
373
+
374
+ uint64_t bits = get_dbits(fp);
375
+
376
+ bool nan = (bits & expmask) == expmask;
377
+
378
+ if (!nan) {
379
+ return 0;
380
+ }
381
+
382
+ if (bits & fracmask) {
383
+ dest[0] = 'n';
384
+ dest[1] = 'a';
385
+ dest[2] = 'n';
386
+
387
+ } else {
388
+ dest[0] = 'i';
389
+ dest[1] = 'n';
390
+ dest[2] = 'f';
391
+ }
392
+
393
+ return 3;
394
+ }
395
+
396
+ /* Fast and accurate double to string conversion based on Florian
397
+ * Loitsch's Grisu-algorithm. The generated string is never longer
398
+ * than 32 characters and is not null terminated. Returns the number
399
+ * of written characters. */
400
+ static int fpconv_dtoa(double d, char dest[32]) {
401
+ char digits[18];
402
+
403
+ int str_len = 0;
404
+ bool neg = false;
405
+
406
+ if (get_dbits(d) & signmask) {
407
+ dest[0] = '-';
408
+ str_len++;
409
+ neg = true;
410
+ }
411
+
412
+ int spec = filter_special(d, dest + str_len);
413
+
414
+ if (spec) {
415
+ return str_len + spec;
416
+ }
417
+
418
+ int K = 0;
419
+ int ndigits = grisu2(d, digits, &K);
420
+
421
+ str_len += emit_digits(digits, ndigits, dest + str_len, K, neg);
422
+
423
+ return str_len;
424
+ }
425
+
426
+ int yep_d2s_json(double d, char* buf) {
427
+ return fpconv_dtoa(d, buf);
428
+ }
@@ -377,7 +377,10 @@ static void emit_canonical_scalar(yep_writer* w, const yep_dnode* n) {
377
377
  tmp[tl] = '\0';
378
378
  double d = strtod(tmp, &end);
379
379
  if (end != NULL && *end == '\0' && end != tmp) {
380
- int wl = yep_d2s_shortest(d, buf);
380
+ /* JSON floats carry the json-gem byte shape so
381
+ * JSON.generate round trips stay byte-identical
382
+ * (yeptris#651); YAML keeps the shortest shape. */
383
+ int wl = w->json ? yep_d2s_json(d, buf) : yep_d2s_shortest(d, buf);
381
384
  wr_put(w, buf, (uint32_t)wl);
382
385
  return;
383
386
  }
@@ -656,6 +656,171 @@ static YeptrisStatus map_fail(int rc) {
656
656
  return rc == -1 ? YEPTRIS_ERROR_MEMORY : YEPTRIS_ERROR_UNSUPPORTED;
657
657
  }
658
658
 
659
+ /* ---- strict duplicate-key pre-scan (yeptris-ruby#278) ----------------
660
+ *
661
+ * json 3 makes duplicate keys an error by DEFAULT; the marshal route
662
+ * must raise before any bytes are written, exactly like the native
663
+ * materializer's rb_hash_aref probe. One linear pass over the same
664
+ * records the writer consumes — the writer itself stays untouched
665
+ * (the dup law lives here, once). The JSON DOM keeps both pairs
666
+ * ("{\"a\":1,\"a\":2}" map_count=2), so the records see them; the
667
+ * non-strict Marshal stream stays last-wins, which is json 2.x's
668
+ * contract.
669
+ *
670
+ * Key identity is the record's typed payload: span kinds compare the
671
+ * arena bytes, the rest compare (kind, b, payload). One growable
672
+ * open-addressing table serves the INNERMOST open map (cleared on
673
+ * close) — duplicates are per-map by definition.
674
+ */
675
+
676
+ typedef struct {
677
+ uint64_t p; /* 0 = empty slot (payload 0 with kind 0 cannot be a key) */
678
+ uint32_t off, len;
679
+ uint8_t kind, b;
680
+ } dup_slot;
681
+
682
+ typedef struct {
683
+ dup_slot* slots;
684
+ size_t cap; /* power of two */
685
+ } dup_set;
686
+
687
+ static uint64_t dup_hash(const dup_slot* k, const char* arena) {
688
+ uint64_t h = 1469598103934665603ull;
689
+ if (k->len != UINT32_MAX && (k->kind == YEP_V_STR || k->kind == YEP_V_TIMESTAMP ||
690
+ k->kind == YEP_V_ALIAS || k->kind == YEP_V_ANCHOR)) {
691
+ for (uint32_t i = 0; i < k->len; i++) {
692
+ h ^= (unsigned char)arena[k->off + i];
693
+ h *= 1099511628211ull;
694
+ }
695
+ }
696
+ h ^= k->kind;
697
+ h *= 1099511628211ull;
698
+ h ^= k->b;
699
+ h *= 1099511628211ull;
700
+ h ^= k->p;
701
+ h *= 1099511628211ull;
702
+ return h;
703
+ }
704
+
705
+ static int dup_eq(const dup_slot* a, const dup_slot* b, const char* arena) {
706
+ if (a->kind != b->kind || a->b != b->b || a->p != b->p) {
707
+ return 0;
708
+ }
709
+ int spanned = (a->kind == YEP_V_STR || a->kind == YEP_V_TIMESTAMP || a->kind == YEP_V_ALIAS ||
710
+ a->kind == YEP_V_ANCHOR);
711
+ if (!spanned) {
712
+ return 1;
713
+ }
714
+ if (a->len != b->len) {
715
+ return 0;
716
+ }
717
+ return a->len == 0 || memcmp(arena + a->off, arena + b->off, a->len) == 0;
718
+ }
719
+
720
+ static int dup_set_grow(dup_set* s) {
721
+ size_t ncap = s->cap ? s->cap * 2 : 16;
722
+ dup_slot* ns = calloc(ncap, sizeof(*ns));
723
+ if (ns == NULL) {
724
+ return -1;
725
+ }
726
+ if (s->slots != NULL) {
727
+ free(s->slots);
728
+ }
729
+ s->slots = ns;
730
+ s->cap = ncap;
731
+ return 0;
732
+ }
733
+
734
+ /* 1 = duplicate found (TLS error set), 0 = clean, -1 = oom */
735
+ static int strict_dup_scan(const yep_value_ctx* c) {
736
+ dup_set s = {NULL, 0};
737
+ int rc = 0;
738
+ const YeptrisValue* vals = c->vals;
739
+ for (size_t i = 0; i < c->n && rc == 0; i++) {
740
+ const YeptrisValue* v = &vals[i];
741
+ if (v->kind == YEP_V_MAP_OPEN) {
742
+ if (s.slots != NULL) {
743
+ memset(s.slots, 0, s.cap * sizeof(*s.slots)); /* innermost map only */
744
+ }
745
+ continue;
746
+ }
747
+ if (v->kind == YEP_V_CLOSE || v->is_key != 1) {
748
+ continue;
749
+ }
750
+ dup_slot k = {v->p, v->off, v->len, v->kind, v->b};
751
+ if (k.p == 0) {
752
+ k.p = 1; /* never aliases the empty-slot sentinel */
753
+ }
754
+ if (s.cap == 0 && dup_set_grow(&s) != 0) {
755
+ rc = -1;
756
+ break;
757
+ }
758
+ uint64_t h = dup_hash(&k, c->arena);
759
+ for (;;) {
760
+ dup_slot* slot = &s.slots[h & (s.cap - 1)];
761
+ if (slot->p == 0) {
762
+ *slot = k;
763
+ break;
764
+ }
765
+ if (dup_eq(slot, &k, c->arena)) {
766
+ yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
767
+ "duplicate key in object");
768
+ rc = 1;
769
+ break;
770
+ }
771
+ h++;
772
+ }
773
+ }
774
+ free(s.slots);
775
+ return rc;
776
+ }
777
+
778
+ static int dom_subtree_tagged(const yep_dom* d, uint32_t id);
779
+ static YeptrisStatus marshal_records(const yep_value_ctx* c, int compat, YeptrisMarshalMode mode,
780
+ int node_mode, char** out, size_t* out_len);
781
+
782
+ YEPTRIS_API YeptrisStatus yeptris_marshal_node_ex(YeptrisNode node, uint32_t flags, char** out,
783
+ size_t* out_len) {
784
+ if (node == NULL || out == NULL || out_len == NULL ||
785
+ (flags & ~YEPTRIS_MARSHAL_STRICT_DUP_KEYS) != 0) {
786
+ return YEPTRIS_ERROR_ARG;
787
+ }
788
+ *out = NULL;
789
+ *out_len = 0;
790
+ yeptris_node* h = (yeptris_node*)node;
791
+ yep_dom* dom = yep_doc_dom(h->doc); /* #378: a lazy parse carries the
792
+ * tape — the tree materializes here */
793
+ if (dom == NULL) {
794
+ return YEPTRIS_ERROR_MEMORY; /* the lazy route's retry contract */
795
+ }
796
+ if (dom_subtree_tagged(dom, h->id) != 0) {
797
+ yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
798
+ "marshal: explicitly tagged node not expressible; "
799
+ "fall back to the value walk");
800
+ return YEPTRIS_ERROR_UNSUPPORTED;
801
+ }
802
+ yep_value_ctx* c = NULL;
803
+ if (yep_values_from_dom(dom, h->id, 0, &c) != 0) {
804
+ return YEPTRIS_ERROR_MEMORY;
805
+ }
806
+ if ((flags & YEPTRIS_MARSHAL_STRICT_DUP_KEYS) != 0) {
807
+ int drc = strict_dup_scan(c);
808
+ if (drc != 0) {
809
+ yep_value_ctx_free(c);
810
+ return drc < 0 ? YEPTRIS_ERROR_MEMORY : YEPTRIS_ERROR_PARSE;
811
+ }
812
+ }
813
+ YeptrisStatus st = marshal_records(c, h->doc->schema == YEPTRIS_SCHEMA_11_COMPAT,
814
+ YEPTRIS_MARSHAL_ALL_DOCS, 1, out, out_len);
815
+ if (st == YEPTRIS_ERROR_UNSUPPORTED) {
816
+ yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
817
+ "marshal: merge key, timestamp or forward alias not expressible; "
818
+ "fall back to the value walk");
819
+ }
820
+ yep_value_ctx_free(c);
821
+ return st;
822
+ }
823
+
659
824
  static YeptrisStatus marshal_records(const yep_value_ctx* c, int compat, YeptrisMarshalMode mode,
660
825
  int node_mode, char** out, size_t* out_len) {
661
826
  *out = NULL;
@@ -750,36 +915,7 @@ static int dom_subtree_tagged(const yep_dom* d, uint32_t id) {
750
915
  }
751
916
 
752
917
  YEPTRIS_API YeptrisStatus yeptris_marshal_node(YeptrisNode node, char** out, size_t* out_len) {
753
- if (node == NULL || out == NULL || out_len == NULL) {
754
- return YEPTRIS_ERROR_ARG;
755
- }
756
- *out = NULL;
757
- *out_len = 0;
758
- yeptris_node* h = (yeptris_node*)node;
759
- yep_dom* dom = yep_doc_dom(h->doc); /* #378: a lazy parse carries the
760
- * tape — the tree materializes here */
761
- if (dom == NULL) {
762
- return YEPTRIS_ERROR_MEMORY; /* the lazy route's retry contract */
763
- }
764
- if (dom_subtree_tagged(dom, h->id) != 0) {
765
- yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
766
- "marshal: explicitly tagged node not expressible; "
767
- "fall back to the value walk");
768
- return YEPTRIS_ERROR_UNSUPPORTED;
769
- }
770
- yep_value_ctx* c = NULL;
771
- if (yep_values_from_dom(dom, h->id, 0, &c) != 0) {
772
- return YEPTRIS_ERROR_MEMORY;
773
- }
774
- YeptrisStatus st = marshal_records(c, h->doc->schema == YEPTRIS_SCHEMA_11_COMPAT,
775
- YEPTRIS_MARSHAL_ALL_DOCS, 1, out, out_len);
776
- if (st == YEPTRIS_ERROR_UNSUPPORTED) {
777
- yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
778
- "marshal: merge key, timestamp or forward alias not expressible; "
779
- "fall back to the value walk");
780
- }
781
- yep_value_ctx_free(c);
782
- return st;
918
+ return yeptris_marshal_node_ex(node, 0, out, out_len);
783
919
  }
784
920
 
785
921
  YEPTRIS_API void yeptris_marshal_free(char* out) {
@@ -3684,9 +3684,15 @@ static int e_parse_value(yep_engine* e, yep_ctx ctx, uint16_t floor_col) {
3684
3684
  continue;
3685
3685
  }
3686
3686
  int pc_comment = (pc == '#' && (prev == ' ' || prev == '\t'));
3687
+ /* an opening quote is node CONTENT, not a prop: the walk
3688
+ * must stop there ('only_props' stays 0) — walking on put
3689
+ * the line's ' #' inside the quoted span, read the quoted
3690
+ * key of the nightly's run 39223056891 ("x:\n!t \"a #b\": y")
3691
+ * as a comment and rejected the keyed pair */
3687
3692
  if (pc == '&' || pc == '!' || pc == '<' || pc == ',' || pc == '>' ||
3688
3693
  (pc != '\n' && pc != '\r' && !pc_comment &&
3689
- !yep_ct_is((unsigned char)pc, YEP_CT_FLOW_IND) && pc != ':' && pc != ' ')) {
3694
+ !yep_ct_is((unsigned char)pc, YEP_CT_FLOW_IND) && pc != ':' && pc != ' ' &&
3695
+ pc != '"' && pc != '\'')) {
3690
3696
  prev = pc;
3691
3697
  t++;
3692
3698
  continue;
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.33.1
4
+ version: 0.6.34.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-10-19 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -123,6 +123,7 @@ files:
123
123
  - vendor/libyeptris/src/yeptris/emit/float/api.h
124
124
  - vendor/libyeptris/src/yeptris/emit/float/dragon.c
125
125
  - vendor/libyeptris/src/yeptris/emit/float/floatint.h
126
+ - vendor/libyeptris/src/yeptris/emit/float/fpconv.c
126
127
  - vendor/libyeptris/src/yeptris/emit/float/print.c
127
128
  - vendor/libyeptris/src/yeptris/emit/style.c
128
129
  - vendor/libyeptris/src/yeptris/emit/style.h