yeptris 0.6.19.1 → 0.6.20.2

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: 74a3c58be8335264316d965e0dd3e2e3a63e8861ee05992a957356181530197f
4
- data.tar.gz: a33173c381a08cc0f752d2919d2c8abe3f7c7f1660f221a4093afb9f191c2d47
3
+ metadata.gz: 1d91dc30d6dd414d0b22ce84b0e573850efa6a86e7d6fcef4425e2d4afc2fca2
4
+ data.tar.gz: 9efe2971ff5b4159b077fba5330a12c0c2ac91bb6849c36c86a83ddeba243a86
5
5
  SHA512:
6
- metadata.gz: 7564355e04a377764e2116bbac676b6a8f3a8cd4aea0bd869f753386bf9afa29da645181ca29b61ff783beeea3cea341cc999a9d4cc4fdf72c22912c4b3b9721
7
- data.tar.gz: e8a2eb033cf888ec75e6657f3ef186a94ded424aeef5e88e63bfd6b7f2c353514931fe2775f19e158722d9c94d10d713bef625a8a7a02fe6af521c1523cc8fe7
6
+ metadata.gz: b40b768c6103cc5715059d7f93350c8c2f9298f86297e2133cf339ec269529237a964bcab0ef5a6ea998a609b5370ac640ba6d195b741bd9382037e2a3114a83
7
+ data.tar.gz: ecfe46a3b63e70b38c11541f6926b7e59a622e8450401b0ff271a362d854317f4bc40647b9ffa2bda3233db3b1e3f09470385f8768f27b794b9655a387123ed4
@@ -1,16 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Builds the native materializer DLL for the CURRENT Ruby and
4
- # installs it under its minor-versioned name
5
- # (lib/yeptris/native-<major.minor>.so).
3
+ # Builds the native materializer for the CURRENT Ruby and installs it
4
+ # under the per-minor DIRECTORY layout
5
+ # (lib/yeptris/<major.minor>/native.<so|bundle|dll>).
6
6
  #
7
7
  # The leptris-ruby Windows lesson (#207/#227): a PE DLL cannot
8
8
  # resolve Ruby imports lazily — it must bind the build Ruby's
9
- # x64-ucrt-rubyNNN.dll. One DLL per supported minor therefore ships
10
- # in the Windows platform gems, and the loader in lib/yeptris.rb
11
- # picks by RUBY_VERSION at require. Ruby 3.3 has no arm64 build:
12
- # that cell ships no artifact and falls back loudly to the FFI
13
- # ladder.
9
+ # x64-ucrt-rubyNNN.dll. One bundle per supported minor therefore ships
10
+ # in the platform gems, and the loader in lib/yeptris.rb picks by
11
+ # RUBY_VERSION at require. Ruby 3.3 has no arm64 build: that cell
12
+ # ships no artifact and falls back loudly to the FFI ladder.
13
+ #
14
+ # The MINOR goes in the directory and the file name stays "native":
15
+ # ruby derives the init symbol from the feature basename, so the old
16
+ # native-<minor>.so name made require ask for the untypeable
17
+ # "Init_native-3" and no shipped bundle could ever load (#157).
18
+ #
19
+ # Unix bundles link the same shape build-platform-gem.sh uses — no
20
+ # libruby DT_NEEDED (its absolute build-runner path dangles on user
21
+ # machines; Ruby API symbols resolve from the host process), and
22
+ # darwin links with -undefined dynamic_lookup.
14
23
  #
15
24
  # Standalone by design: the release workflow runs this under each
16
25
  # Ruby minor (3.3/3.4/4.0) with no bundler context.
@@ -21,21 +30,31 @@ require "fileutils"
21
30
  root = File.expand_path("..", __dir__)
22
31
  ext_dir = File.join(root, "ext", "yeptris_native")
23
32
  minor = RUBY_VERSION[/\A\d+\.\d+/]
33
+ windows = RbConfig::CONFIG["host_os"] =~ /mingw|mswin/
24
34
 
25
35
  Dir.chdir(ext_dir) do
26
36
  system(RbConfig.ruby, "extconf.rb") or abort "extconf failed under #{RUBY_VERSION}"
27
37
  # mkmf's link binds the CURRENT Ruby's runtime DLL — exactly
28
- # what the versioned naming is for.
29
- success = system("make")
38
+ # what the per-minor scheme is for.
39
+ if windows
40
+ success = system("make")
41
+ else
42
+ darwin = RbConfig::CONFIG["host_os"] =~ /darwin/
43
+ dld = darwin ? "-dynamic -bundle -undefined dynamic_lookup" : ""
44
+ success = system("make", "LIBRUBYARG_SHARED=", "LIBRUBYARG_STATIC=",
45
+ "DLDFLAGS=#{dld}")
46
+ end
30
47
  abort "make failed under #{RUBY_VERSION}" unless success
31
- so = Dir.glob("native.{so,dll}").first
48
+ so = Dir.glob("native.{so,bundle,dll}").first
32
49
  abort "native bundle not produced under #{RUBY_VERSION}" unless so
33
- dest = File.join(root, "lib", "yeptris", "native-#{minor}.so")
50
+ dest_dir = File.join(root, "lib", "yeptris", minor)
51
+ dest = File.join(dest_dir, File.basename(so))
34
52
  # The artifact must reference only this minor's Ruby DLL.
35
53
  imported = `strings #{so} 2>/dev/null`[/[a-z0-9-]*ruby\d{3,}\.dll/i]
36
54
  if imported && !imported.include?("ruby#{minor.delete('.')}")
37
55
  abort "#{so} imports #{imported} but was built under #{RUBY_VERSION} — refuse to mis-name it"
38
56
  end
57
+ FileUtils.mkdir_p(dest_dir)
39
58
  FileUtils.cp(so, dest)
40
59
  puts "Installed native materializer for Ruby #{minor} -> #{dest} (#{imported || 'no ruby dll string found'})"
41
60
  end
data/lib/yeptris/psych.rb CHANGED
@@ -619,7 +619,7 @@ end
619
619
  # rebind ::Psych. It does not anymore, and re-binding it here would
620
620
  # regress #69 (any stdlib psych loaded afterwards would explode with
621
621
  # a superclass mismatch), so the old path warns instead of acting.
622
- if defined?(::Psych) && !::Psych.equal?(Yeptris::Psych)
622
+ if defined?(::Psych) && !::Psych.equal?(Yeptris::Psych) && Yeptris.debug?
623
623
  warn "yeptris: \"yeptris/psych\" defines the namespace only — it no " \
624
624
  "longer rebinds ::Psych. Require \"yeptris/psych/drop_in\" for the " \
625
625
  "drop-in rebind, or call Yeptris::Psych explicitly."
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,17 @@
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.19.1".freeze
6
+ VERSION = "0.6.20.2".freeze
7
+
8
+ # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
+ # embed yeptris inside CLIs that assert clean stderr, and a
10
+ # successful fallback is not a warning. YEPTRIS_DEBUG=1 turns the
11
+ # notices on.
12
+ def self.debug?
13
+ return @debug unless @debug.nil?
14
+ v = ENV["YEPTRIS_DEBUG"]
15
+ @debug = !v.nil? && !v.empty? && v != "0"
16
+ end
7
17
  # The error hierarchy lives in THIS file (the parent namespace's
8
18
  # own file): nested constants do not trigger a parent-constant
9
19
  # autoload, and the law forbids internal requires — defining the
@@ -91,23 +101,29 @@ end
91
101
  # Optional C-API materializer (TODO.restructure/22): fused visit →
92
102
  # Ruby objects via the Ruby C API. Feature-detected — LoadError leaves
93
103
  # the FFI ladder (Marshal → columns → records) as the sole path.
94
- # Windows names the artifact per Ruby minor (the leptris-ruby #207/
95
- # #227 lesson): a PE DLL must bind its build Ruby's runtime, so one
96
- # DLL per supported minor ships and RUBY_VERSION selects. A missing
97
- # cell (Ruby 3.3 arm64 has no build) falls back LOUDLY to the ladder.
104
+ # One bundle per Ruby minor (the leptris-ruby #207/#227 lesson: a PE
105
+ # DLL must bind its build Ruby's runtime; a unix bundle is also
106
+ # minor-locked by the internal API it compiles against). The MINOR
107
+ # lives in the DIRECTORY (lib/yeptris/<minor>/native.<ext>) and the
108
+ # FILE name stays unversioned: ruby derives the init symbol from the
109
+ # feature basename, so native-3.4.so asked for the untypeable
110
+ # "Init_native-3" and the versioned-file scheme could never load
111
+ # (#157 — every shipped bundle was dead code). Dev checkouts keep
112
+ # the plain native.<ext> name (mkmf layout, built for the running
113
+ # Ruby). A missing cell falls back LOUDLY to the ladder.
98
114
  begin
99
- # per-minor FIRST everywhere: a single version-agnostic native.so
100
- # loads under ANY Ruby (symbols resolve from the host process), and
101
- # one built for a different minor is an ABI gamble — Ruby 3.0 ran
102
- # a 3.3-built ext and the JSON parse failed. The plain name stays
103
- # as the dev-checkout fallback (built for the running Ruby).
104
- require "yeptris/native-#{RUBY_VERSION[/\A\d+\.\d+/]}"
115
+ # per-minor FIRST: a single version-agnostic native.so loads under
116
+ # ANY Ruby and is an ABI gamble across minors — Ruby 3.0 ran a
117
+ # 3.3-built ext and the JSON parse failed.
118
+ require "yeptris/#{RUBY_VERSION[/\A\d+\.\d+/]}/native"
105
119
  rescue LoadError
106
120
  begin
107
121
  require "yeptris/native"
108
122
  rescue LoadError => e
109
- warn "yeptris: no precompiled native materializer for Ruby " \
110
- "#{RUBY_VERSION[/\A\d+\.\d+/]} on this platform " \
111
- "(#{e.message}); the FFI ladder carries the load"
123
+ if Yeptris.debug? # the require chain is top-level (self = main)
124
+ warn "yeptris: no precompiled native materializer for Ruby " \
125
+ "#{RUBY_VERSION[/\A\d+\.\d+/]} on this platform " \
126
+ "(#{e.message}); the FFI ladder carries the load"
127
+ end
112
128
  end
113
129
  end
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.19
6
+ VERSION 0.6.20
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -24,6 +24,7 @@ set(YEPTRIS_SOURCES
24
24
  yeptris/parse/scalars.c
25
25
  yeptris/parse/engine.c
26
26
  yeptris/dom/dom.c
27
+ yeptris/dom/ytape.c
27
28
  yeptris/dom/hpool.c
28
29
  yeptris/dom/mapindex.c
29
30
  yeptris/dom/mutate.c
@@ -31,6 +31,7 @@ YEPTRIS_API YeptrisDocument yeptris_document_new(void) {
31
31
  yep_dom_destroy(dom);
32
32
  return NULL;
33
33
  }
34
+ yep_mutex_init(&doc->lazy_mu);
34
35
  memset(doc, 0, sizeof(*doc));
35
36
  doc->dom = dom;
36
37
  doc->sys = sys;
@@ -860,6 +860,7 @@ static YeptrisDocument cbor_wrap(yep_dom* dom, const void* buf, const yep_alloca
860
860
  yep_dom_destroy(dom);
861
861
  return NULL;
862
862
  }
863
+ yep_mutex_init(&d->lazy_mu);
863
864
  d->dom = dom;
864
865
  d->sys = sys;
865
866
  d->schema = YEPTRIS_SCHEMA_12_CORE;
@@ -868,6 +869,7 @@ static YeptrisDocument cbor_wrap(yep_dom* dom, const void* buf, const yep_alloca
868
869
  d->input = (const char*)buf;
869
870
  d->finish_pool = NULL;
870
871
  d->lazy_tape = NULL; /* field-by-field ctor: no garbage for free */
872
+ d->lazy_kind = 0;
871
873
  return (YeptrisDocument)d;
872
874
  }
873
875
 
@@ -966,6 +968,7 @@ YEPTRIS_API YeptrisDocument yeptris_cbor_decode(const void* buf, size_t len, uin
966
968
  }
967
969
  return NULL;
968
970
  }
971
+ yep_mutex_init(&doc->lazy_mu);
969
972
  doc->dom = dom;
970
973
  doc->sys = sys;
971
974
  doc->schema = YEPTRIS_SCHEMA_12_CORE; /* CBOR's model is the core schema */
@@ -99,6 +99,15 @@ typedef struct yep_chunk_masks {
99
99
  uint32_t valid; /* 1-bits for the bytes that exist (tails) */
100
100
  } yep_chunk_masks;
101
101
 
102
+ /* one 64-byte block's classification (json_stage1_masks) */
103
+ struct yep_s1_block {
104
+ uint64_t q; /* raw quote bytes */
105
+ uint64_t bs; /* backslash bytes */
106
+ uint64_t op; /* []{}:, */
107
+ uint64_t c0; /* bytes < 0x20 (ws included) */
108
+ uint64_t tokens; /* the resolver's verdict: ops + string-open quotes */
109
+ };
110
+
102
111
  /* The kernel table. One struct = one dispatch point (OCP: a new ISA is a
103
112
  * new TU exporting a new table; nothing else changes). */
104
113
  typedef struct yep_text_kernels {
@@ -175,10 +184,25 @@ typedef struct yep_text_kernels {
175
184
  * hard error is an unterminated string (odd final parity) ->
176
185
  * return 0. idx must hold len+2 entries. Bit-identical across
177
186
  * ISAs (the differential suite pins it); scan/json.c owns the
178
- * scalar reference and the grammar contract. */
179
- int (*json_stage1)(const char* p, size_t len, uint32_t* idx, size_t* nidx);
187
+ * grammar contract. flags (never NULL) accumulates YEP_S1_* bits:
188
+ * the doc-level facts stage 2 needs to skip content scanning. */
189
+ int (*json_stage1)(const char* p, size_t len, uint32_t* idx, size_t* nidx, unsigned* flags);
190
+
191
+ /* The structural indexer's classification half, without the
192
+ * emission: per 64-byte block, the four u64 masks the resolver
193
+ * consumes (QUOTE/BS/OP/C0; ws is dead since the token contract
194
+ * dropped scalar-run starts). blocks receives len/64+1 records;
195
+ * *nblocks returns how many were written. Same reject/flag
196
+ * contract as json_stage1. The single-pass tape walk consumes
197
+ * this — no idx array, no emission loop. */
198
+ int (*json_stage1_masks)(const char* p, size_t len, struct yep_s1_block* blocks,
199
+ size_t* nblocks, unsigned* flags);
180
200
  } yep_text_kernels;
181
201
 
202
+ /* json_stage1 flag bits (accumulated across blocks; cleared by caller) */
203
+ #define YEP_S1_C0_IN_STRING 1u /* raw non-ws C0 inside a string: reject */
204
+ #define YEP_S1_HAS_ESCAPE 2u /* a backslash byte exists: validate spans */
205
+
182
206
  /* The structural indexer's mask resolver — the SSOT identity set for
183
207
  * every json_stage1 implementation (scalar in scan/json.c, the ISA
184
208
  * twins in the SIMD TUs). Given one 64-byte block's QUOTE/BS/OP/WS
@@ -195,15 +219,13 @@ typedef struct yep_text_kernels {
195
219
  * tokens = (op | starts) & ~string_tail
196
220
  *
197
221
  * Returns the block's contribution; carries update in place. */
198
- static inline size_t yep_json_stage1_resolve(uint64_t q, uint64_t bs, uint64_t op, uint64_t ws,
199
- uint64_t valid, uint64_t* prev_in_string,
200
- uint64_t* esc_carry, uint64_t* follows_carry,
201
- size_t off, uint32_t* idx, size_t n) {
222
+ static inline uint64_t yep_json_stage1_tokens(uint64_t q, uint64_t bs, uint64_t op, uint64_t c0,
223
+ uint64_t valid, uint64_t* prev_in_string,
224
+ uint64_t* esc_carry, unsigned* flags) {
202
225
  uint64_t potential = bs & ~*esc_carry;
203
226
  uint64_t s = (((potential << 1) | 0xAAAAAAAAAAAAAAAAull) - potential) ^ 0xAAAAAAAAAAAAAAAAull;
204
- uint64_t esc_bs = s & bs;
205
227
  uint64_t escaped = s ^ (bs | *esc_carry);
206
- *esc_carry = esc_bs >> 63;
228
+ *esc_carry = (s & bs) >> 63;
207
229
  uint64_t quote_live = q & ~escaped;
208
230
  uint64_t ps = quote_live; /* sequential prefix-xor — a flat
209
231
  * one-expression xor of shifts misses
@@ -217,15 +239,27 @@ static inline size_t yep_json_stage1_resolve(uint64_t q, uint64_t bs, uint64_t o
217
239
  uint64_t in_string = ps ^ *prev_in_string;
218
240
  *prev_in_string = (uint64_t)((int64_t)in_string >> 63);
219
241
  uint64_t string_tail = (in_string ^ quote_live) & valid;
220
- uint64_t scalar_nq = (~(op | ws) & ~q) & valid; /* non-quote scalars */
221
- uint64_t follows = ((scalar_nq & ~(1ull << 63)) << 1) | *follows_carry;
222
- *follows_carry = scalar_nq >> 63;
223
- uint64_t tokens = ((op | (~(op | ws) & ~follows)) & ~string_tail) & valid;
242
+ if (bs != 0) {
243
+ *flags |= YEP_S1_HAS_ESCAPE;
244
+ }
245
+ if ((c0 & in_string & valid) != 0) {
246
+ *flags |= YEP_S1_C0_IN_STRING;
247
+ }
248
+ return (op | (quote_live & ~string_tail)) & ~string_tail & valid;
249
+ }
250
+
251
+ /* The full resolver: token mask derivation + the idx emission loop
252
+ * (the json_stage1 implementations). */
253
+ static inline size_t yep_json_stage1_resolve(uint64_t q, uint64_t bs, uint64_t op, uint64_t ws,
254
+ uint64_t c0, uint64_t valid, uint64_t* prev_in_string,
255
+ uint64_t* esc_carry, size_t off, uint32_t* idx,
256
+ size_t n, unsigned* flags) {
257
+ uint64_t tokens =
258
+ yep_json_stage1_tokens(q, bs, op, c0, valid, prev_in_string, esc_carry, flags);
224
259
  while (tokens != 0) {
225
260
  idx[n++] = (uint32_t)(off + (size_t)yep_ctz64(tokens));
226
261
  tokens &= tokens - 1;
227
262
  }
228
- (void)escaped;
229
263
  return n;
230
264
  }
231
265
 
@@ -455,12 +455,13 @@ yep_chunk_masks yep_text_json_chunk_avx2(const char* p, size_t n) {
455
455
  * (two 32-byte lane passes each, movemask-native), then the shared
456
456
  * mask resolver (simd_text.h). The final partial block builds its
457
457
  * masks byte-wise — no loads past len. */
458
- int yep_text_json_stage1_avx2(const char* p, size_t len, uint32_t* idx, size_t* nidx) {
458
+ int yep_text_json_stage1_avx2(const char* p, size_t len, uint32_t* idx, size_t* nidx,
459
+ unsigned* flags) {
459
460
  size_t n = 0;
460
- uint64_t prev_in_string = 0, esc_carry = 0, follows_carry = 0;
461
+ uint64_t prev_in_string = 0, esc_carry = 0;
461
462
  size_t off = 0;
462
463
  for (; off + 64 <= len; off += 64) {
463
- uint64_t q = 0, bs = 0, op = 0, ws = 0;
464
+ uint64_t q = 0, bs = 0, op = 0, ws = 0, c0 = 0;
464
465
  for (unsigned half = 0; half < 2; half++) {
465
466
  const __m256i v =
466
467
  _mm256_loadu_si256((const __m256i*)(const void*)(p + off + 32 * half));
@@ -482,17 +483,21 @@ int yep_text_json_stage1_avx2(const char* p, size_t len, uint32_t* idx, size_t*
482
483
  _mm256_or_si256(_mm256_cmpeq_epi8(v, _mm256_set1_epi8('\n')),
483
484
  _mm256_cmpeq_epi8(v, _mm256_set1_epi8('\r'))));
484
485
  uint64_t fw = (uint64_t)(uint32_t)_mm256_movemask_epi8(w);
486
+ /* unsigned min(v, 0x20) == v <=> v < 0x20 */
487
+ uint64_t fc = (uint64_t)(uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(
488
+ _mm256_max_epu8(v, _mm256_set1_epi8(0x1F)), _mm256_set1_epi8(0x1F)));
485
489
  q |= fq << (32 * half);
486
490
  bs |= fb << (32 * half);
487
491
  op |= fo << (32 * half);
488
492
  ws |= fw << (32 * half);
493
+ c0 |= fc << (32 * half);
489
494
  }
490
- n = yep_json_stage1_resolve(q, bs, op, ws, ~0ull, &prev_in_string, &esc_carry,
491
- &follows_carry, off, idx, n);
495
+ n = yep_json_stage1_resolve(q, bs, op, ws, c0, ~0ull, &prev_in_string, &esc_carry, off, idx,
496
+ n, flags);
492
497
  }
493
498
  if (off < len) {
494
499
  size_t cn = len - off;
495
- uint64_t q = 0, bs = 0, op = 0, ws = 0;
500
+ uint64_t q = 0, bs = 0, op = 0, ws = 0, c0 = 0;
496
501
  for (size_t k = 0; k < cn; k++) {
497
502
  unsigned char c = (unsigned char)p[off + k];
498
503
  uint64_t bit = 1ull << k;
@@ -505,14 +510,86 @@ int yep_text_json_stage1_avx2(const char* p, size_t len, uint32_t* idx, size_t*
505
510
  } else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
506
511
  ws |= bit;
507
512
  }
513
+ if (c < 0x20) {
514
+ c0 |= bit;
515
+ }
508
516
  }
509
- n = yep_json_stage1_resolve(q, bs, op, ws, (1ull << cn) - 1ull, &prev_in_string, &esc_carry,
510
- &follows_carry, off, idx, n);
517
+ n = yep_json_stage1_resolve(q, bs, op, ws, c0, (1ull << cn) - 1ull, &prev_in_string,
518
+ &esc_carry, off, idx, n, flags);
511
519
  }
512
520
  *nidx = n;
513
521
  return prev_in_string ? 0 : 1;
514
522
  }
515
523
 
524
+ /* The classification half of the structural indexer (AVX2 twin of the
525
+ * NEON masks slot): two 32-byte lane passes per 64-byte block. */
526
+ int yep_text_json_stage1_masks_avx2(const char* p, size_t len, struct yep_s1_block* blocks,
527
+ size_t* nblocks, unsigned* flags) {
528
+ size_t nb = 0;
529
+ uint64_t prev_in_string = 0, esc_carry = 0;
530
+ size_t off = 0;
531
+ for (; off + 64 <= len; off += 64) {
532
+ uint64_t q = 0, bs = 0, op = 0, c0 = 0;
533
+ for (unsigned half = 0; half < 2; half++) {
534
+ const __m256i v =
535
+ _mm256_loadu_si256((const __m256i*)(const void*)(p + off + 32 * half));
536
+ uint64_t fq = (uint64_t)(uint32_t)_mm256_movemask_epi8(
537
+ _mm256_cmpeq_epi8(v, _mm256_set1_epi8('"')));
538
+ uint64_t fb = (uint64_t)(uint32_t)_mm256_movemask_epi8(
539
+ _mm256_cmpeq_epi8(v, _mm256_set1_epi8('\\')));
540
+ __m256i o = _mm256_or_si256(
541
+ _mm256_or_si256(_mm256_cmpeq_epi8(v, _mm256_set1_epi8('{')),
542
+ _mm256_cmpeq_epi8(v, _mm256_set1_epi8('}'))),
543
+ _mm256_or_si256(_mm256_or_si256(_mm256_cmpeq_epi8(v, _mm256_set1_epi8('[')),
544
+ _mm256_cmpeq_epi8(v, _mm256_set1_epi8(']'))),
545
+ _mm256_or_si256(_mm256_cmpeq_epi8(v, _mm256_set1_epi8(',')),
546
+ _mm256_cmpeq_epi8(v, _mm256_set1_epi8(':')))));
547
+ uint64_t fo = (uint64_t)(uint32_t)_mm256_movemask_epi8(o);
548
+ uint64_t fc = (uint64_t)(uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(
549
+ _mm256_max_epu8(v, _mm256_set1_epi8(0x1F)), _mm256_set1_epi8(0x1F)));
550
+ q |= fq << (32 * half);
551
+ bs |= fb << (32 * half);
552
+ op |= fo << (32 * half);
553
+ c0 |= fc << (32 * half);
554
+ }
555
+ blocks[nb].q = q;
556
+ blocks[nb].bs = bs;
557
+ blocks[nb].op = op;
558
+ blocks[nb].c0 = c0;
559
+ blocks[nb].tokens =
560
+ yep_json_stage1_tokens(q, bs, op, c0, ~0ull, &prev_in_string, &esc_carry, flags);
561
+ nb++;
562
+ }
563
+ if (off < len) {
564
+ size_t cn = len - off;
565
+ uint64_t q = 0, bs = 0, op = 0, c0 = 0;
566
+ for (size_t k = 0; k < cn; k++) {
567
+ unsigned char c = (unsigned char)p[off + k];
568
+ uint64_t bit = 1ull << k;
569
+ if (c == '"') {
570
+ q |= bit;
571
+ } else if (c == '\\') {
572
+ bs |= bit;
573
+ } else if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') {
574
+ op |= bit;
575
+ }
576
+ if (c < 0x20) {
577
+ c0 |= bit;
578
+ }
579
+ }
580
+ uint64_t valid = (1ull << cn) - 1ull;
581
+ blocks[nb].q = q;
582
+ blocks[nb].bs = bs;
583
+ blocks[nb].op = op;
584
+ blocks[nb].c0 = c0;
585
+ blocks[nb].tokens =
586
+ yep_json_stage1_tokens(q, bs, op, c0, valid, &prev_in_string, &esc_carry, flags);
587
+ nb++;
588
+ }
589
+ *nblocks = nb;
590
+ return prev_in_string ? 0 : 1;
591
+ }
592
+
516
593
  const yep_text_kernels yep_text_kernels_avx2 = {
517
594
  yep_avx2_contains,
518
595
  yep_avx2_find,
@@ -529,6 +606,7 @@ const yep_text_kernels yep_text_kernels_avx2 = {
529
606
  yep_avx2_line_facts,
530
607
  yep_text_json_chunk_avx2,
531
608
  yep_text_json_stage1_avx2,
609
+ yep_text_json_stage1_masks_avx2,
532
610
  };
533
611
 
534
612
  #endif /* YEP_ARCH_X86 */
@@ -588,9 +588,10 @@ yep_chunk_masks yep_text_json_chunk_neon(const char* p, size_t n) {
588
588
  * (four 16-byte halves each), then the shared mask resolver (the
589
589
  * simdjson identity set, simd_text.h). The final partial block
590
590
  * builds its masks byte-wise — no loads past len. */
591
- int yep_text_json_stage1_neon(const char* p, size_t len, uint32_t* idx, size_t* nidx) {
591
+ int yep_text_json_stage1_neon(const char* p, size_t len, uint32_t* idx, size_t* nidx,
592
+ unsigned* flags) {
592
593
  size_t n = 0;
593
- uint64_t prev_in_string = 0, esc_carry = 0, follows_carry = 0;
594
+ uint64_t prev_in_string = 0, esc_carry = 0;
594
595
  size_t off = 0;
595
596
  const uint8x16_t dq = vdupq_n_u8('"'), dbs = vdupq_n_u8('\\');
596
597
  const uint8x16_t d1 = vdupq_n_u8('{'), d2 = vdupq_n_u8('}');
@@ -598,8 +599,9 @@ int yep_text_json_stage1_neon(const char* p, size_t len, uint32_t* idx, size_t*
598
599
  const uint8x16_t d5 = vdupq_n_u8(','), d6 = vdupq_n_u8(':');
599
600
  const uint8x16_t wsp = vdupq_n_u8(' '), wtab = vdupq_n_u8('\t');
600
601
  const uint8x16_t wnl = vdupq_n_u8('\n'), wcr = vdupq_n_u8('\r');
602
+ const uint8x16_t c0t = vdupq_n_u8(0x20);
601
603
  for (; off + 64 <= len; off += 64) {
602
- uint64_t q = 0, bs = 0, op = 0, ws = 0;
604
+ uint64_t q = 0, bs = 0, op = 0, ws = 0, c0 = 0;
603
605
  for (unsigned half = 0; half < 4; half++) {
604
606
  uint8x16_t v = vld1q_u8((const uint8_t*)(const void*)(p + off + 16 * half));
605
607
  uint64_t fq = yep_neon_bits(vceqq_u8(v, dq));
@@ -610,17 +612,19 @@ int yep_text_json_stage1_neon(const char* p, size_t len, uint32_t* idx, size_t*
610
612
  vorrq_u8(vceqq_u8(v, d5), vceqq_u8(v, d6))));
611
613
  uint64_t fw = yep_neon_bits(vorrq_u8(vorrq_u8(vceqq_u8(v, wsp), vceqq_u8(v, wtab)),
612
614
  vorrq_u8(vceqq_u8(v, wnl), vceqq_u8(v, wcr))));
615
+ uint64_t fc = yep_neon_bits(vcltq_u8(v, c0t));
613
616
  q |= fq << (16 * half);
614
617
  bs |= fb << (16 * half);
615
618
  op |= fo << (16 * half);
616
619
  ws |= fw << (16 * half);
620
+ c0 |= fc << (16 * half);
617
621
  }
618
- n = yep_json_stage1_resolve(q, bs, op, ws, ~0ull, &prev_in_string, &esc_carry,
619
- &follows_carry, off, idx, n);
622
+ n = yep_json_stage1_resolve(q, bs, op, ws, c0, ~0ull, &prev_in_string, &esc_carry, off, idx,
623
+ n, flags);
620
624
  }
621
625
  if (off < len) { /* the tail: byte-wise masks, no loads past len */
622
626
  size_t cn = len - off;
623
- uint64_t q = 0, bs = 0, op = 0, ws = 0;
627
+ uint64_t q = 0, bs = 0, op = 0, ws = 0, c0 = 0;
624
628
  for (size_t k = 0; k < cn; k++) {
625
629
  unsigned char c = (unsigned char)p[off + k];
626
630
  uint64_t bit = 1ull << k;
@@ -633,14 +637,83 @@ int yep_text_json_stage1_neon(const char* p, size_t len, uint32_t* idx, size_t*
633
637
  } else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
634
638
  ws |= bit;
635
639
  }
640
+ if (c < 0x20) {
641
+ c0 |= bit;
642
+ }
636
643
  }
637
- n = yep_json_stage1_resolve(q, bs, op, ws, (1ull << cn) - 1ull, &prev_in_string, &esc_carry,
638
- &follows_carry, off, idx, n);
644
+ n = yep_json_stage1_resolve(q, bs, op, ws, c0, (1ull << cn) - 1ull, &prev_in_string,
645
+ &esc_carry, off, idx, n, flags);
639
646
  }
640
647
  *nidx = n;
641
648
  return prev_in_string ? 0 : 1;
642
649
  }
643
650
 
651
+ /* The classification half of the structural indexer: the same
652
+ * 64-byte/4-half mask build, no emission, blocks out instead of idx. */
653
+ int yep_text_json_stage1_masks_neon(const char* p, size_t len, struct yep_s1_block* blocks,
654
+ size_t* nblocks, unsigned* flags) {
655
+ size_t nb = 0;
656
+ uint64_t prev_in_string = 0, esc_carry = 0;
657
+ size_t off = 0;
658
+ const uint8x16_t dq = vdupq_n_u8('"'), dbs = vdupq_n_u8('\\');
659
+ const uint8x16_t d1 = vdupq_n_u8('{'), d2 = vdupq_n_u8('}');
660
+ const uint8x16_t d3 = vdupq_n_u8('['), d4 = vdupq_n_u8(']');
661
+ const uint8x16_t d5 = vdupq_n_u8(','), d6 = vdupq_n_u8(':');
662
+ const uint8x16_t c0t = vdupq_n_u8(0x20);
663
+ for (; off + 64 <= len; off += 64) {
664
+ uint64_t q = 0, bs = 0, op = 0, c0 = 0;
665
+ for (unsigned half = 0; half < 4; half++) {
666
+ uint8x16_t v = vld1q_u8((const uint8_t*)(const void*)(p + off + 16 * half));
667
+ uint64_t fq = yep_neon_bits(vceqq_u8(v, dq));
668
+ uint64_t fb = yep_neon_bits(vceqq_u8(v, dbs));
669
+ uint64_t fo =
670
+ yep_neon_bits(vorrq_u8(vorrq_u8(vorrq_u8(vceqq_u8(v, d1), vceqq_u8(v, d2)),
671
+ vorrq_u8(vceqq_u8(v, d3), vceqq_u8(v, d4))),
672
+ vorrq_u8(vceqq_u8(v, d5), vceqq_u8(v, d6))));
673
+ uint64_t fc = yep_neon_bits(vcltq_u8(v, c0t));
674
+ q |= fq << (16 * half);
675
+ bs |= fb << (16 * half);
676
+ op |= fo << (16 * half);
677
+ c0 |= fc << (16 * half);
678
+ }
679
+ blocks[nb].q = q;
680
+ blocks[nb].bs = bs;
681
+ blocks[nb].op = op;
682
+ blocks[nb].c0 = c0;
683
+ blocks[nb].tokens =
684
+ yep_json_stage1_tokens(q, bs, op, c0, ~0ull, &prev_in_string, &esc_carry, flags);
685
+ nb++;
686
+ }
687
+ if (off < len) {
688
+ size_t cn = len - off;
689
+ uint64_t q = 0, bs = 0, op = 0, c0 = 0;
690
+ for (size_t k = 0; k < cn; k++) {
691
+ unsigned char c = (unsigned char)p[off + k];
692
+ uint64_t bit = 1ull << k;
693
+ if (c == '"') {
694
+ q |= bit;
695
+ } else if (c == '\\') {
696
+ bs |= bit;
697
+ } else if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') {
698
+ op |= bit;
699
+ }
700
+ if (c < 0x20) {
701
+ c0 |= bit;
702
+ }
703
+ }
704
+ uint64_t valid = (1ull << cn) - 1ull;
705
+ blocks[nb].q = q;
706
+ blocks[nb].bs = bs;
707
+ blocks[nb].op = op;
708
+ blocks[nb].c0 = c0;
709
+ blocks[nb].tokens =
710
+ yep_json_stage1_tokens(q, bs, op, c0, valid, &prev_in_string, &esc_carry, flags);
711
+ nb++;
712
+ }
713
+ *nblocks = nb;
714
+ return prev_in_string ? 0 : 1;
715
+ }
716
+
644
717
  const yep_text_kernels yep_text_kernels_neon = {
645
718
  yep_neon_contains,
646
719
  yep_neon_find,
@@ -657,6 +730,7 @@ const yep_text_kernels yep_text_kernels_neon = {
657
730
  yep_neon_line_facts,
658
731
  yep_text_json_chunk_neon,
659
732
  yep_text_json_stage1_neon,
733
+ yep_text_json_stage1_masks_neon,
660
734
  };
661
735
 
662
736
  #endif /* YEP_ARCH_AARCH64 */
@@ -287,12 +287,16 @@ yep_chunk_masks yep_text_json_chunk_scalar(const char* p, size_t n) {
287
287
 
288
288
  /* the stage-1 scalar reference lives in scan/json.c (the grammar
289
289
  * home); the table carries it so dispatch stays one point */
290
- extern int yep_json_stage1_scalar(const char* p, size_t len, uint32_t* idx, size_t* nidx);
290
+ extern int yep_json_stage1_scalar(const char* p, size_t len, uint32_t* idx, size_t* nidx,
291
+ unsigned* flags);
292
+ extern int yep_json_stage1_masks_scalar(const char* p, size_t len, struct yep_s1_block* blocks,
293
+ size_t* nblocks, unsigned* flags);
291
294
 
292
295
  const yep_text_kernels yep_text_kernels_scalar = {
293
- yep_text_contains_scalar, yep_text_find_scalar, yep_text_find3_scalar,
294
- yep_text_count_char_scalar, yep_text_count3_scalar, yep_text_copy_count3_scalar,
295
- yep_text_find_not_scalar, yep_text_stopset_find_scalar, yep_text_quote_scan_scalar,
296
- yep_text_scan_stats_scalar, yep_text_qbc_find_scalar, yep_text_gate_scan_scalar,
297
- yep_text_line_facts_scalar, yep_text_json_chunk_scalar, yep_json_stage1_scalar,
296
+ yep_text_contains_scalar, yep_text_find_scalar, yep_text_find3_scalar,
297
+ yep_text_count_char_scalar, yep_text_count3_scalar, yep_text_copy_count3_scalar,
298
+ yep_text_find_not_scalar, yep_text_stopset_find_scalar, yep_text_quote_scan_scalar,
299
+ yep_text_scan_stats_scalar, yep_text_qbc_find_scalar, yep_text_gate_scan_scalar,
300
+ yep_text_line_facts_scalar, yep_text_json_chunk_scalar, yep_json_stage1_scalar,
301
+ yep_json_stage1_masks_scalar,
298
302
  };
@@ -7,6 +7,7 @@
7
7
 
8
8
  #include <stddef.h>
9
9
 
10
+ #include "common/mutex.h"
10
11
  #include "dom/dom.h"
11
12
  #include "memory/allocator.h"
12
13
 
@@ -18,15 +19,24 @@ typedef struct yeptris_document {
18
19
  * conditioned on it (TODO.restructure/32) */
19
20
  unsigned char* transcoded; /* owned when non-NULL */
20
21
  size_t transcoded_len;
21
- const char* input; /* borrowed input (lifetime documentation) */
22
- void* finish_pool; /* engine finish pool: resolved tags, folded and
23
- escaped scalars outlive the engine through the
24
- document */
25
- void* lazy_tape; /* #342 slice 2: the parsed tape when the tree is
26
- * deferred (gate-clean strict JSON). dom==NULL until
27
- * the first tree access materializes via dom_from_tape;
28
- * freed with the document. void* to avoid a header
29
- * cycle — parse.c casts to yeptris_json_tape* */
22
+ const char* input; /* borrowed input (lifetime documentation) */
23
+ void* finish_pool; /* engine finish pool: resolved tags, folded and
24
+ escaped scalars outlive the engine through the
25
+ document */
26
+ void* lazy_tape; /* #342 slice 2: the parsed tape when the tree is
27
+ * deferred (gate-clean strict JSON; #378: the YAML
28
+ * record tape). dom==NULL until the first tree
29
+ * access materializes; freed with the document.
30
+ * void* to avoid a header cycle — parse.c casts
31
+ * per lazy_kind */
32
+ int lazy_kind; /* 0 = none/json (yeptris_json_tape), 1 = yaml
33
+ * (yep_ytape) — every constructor must set it
34
+ * (the field-by-field ctor trap) */
35
+ yep_mutex_raw lazy_mu; /* materialization lock: the first tree
36
+ * access builds; concurrent first accesses
37
+ * wait (read-only sharing stays lock-free
38
+ * once dom is set — every constructor must
39
+ * yep_mutex_init it, the ctor trap) */
30
40
  } yeptris_document;
31
41
 
32
42
  /* Node handle: a (document, node-id) pair so nodes stay usable even if