yeptris 0.6.18.2 → 0.6.20.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: 9d2c20e34ee04e8ead36d9dd1d0416a69e3171bdd9d567e724a9781308aa5287
4
- data.tar.gz: 9e2c440e6ab072af22c568dd7de4262b331d3bcd69d61dad25dae64854d6a925
3
+ metadata.gz: e22b16a8d0158fb8161f390d54b7dd8de6612fbfcc881741a7896f0a891b6188
4
+ data.tar.gz: 9fab611eff46efb8a82f434a6daf89fafbd400c3493ee80a36a01fbcba7a8983
5
5
  SHA512:
6
- metadata.gz: b3c9fd2d90bd3490fa8070c47088a10288dd5933098926a3a703ad9e78fd44a06bc2fd38c3f5cd4b373ca056faa6db6f0d4004a06d415be964470b6989423acd
7
- data.tar.gz: 89da80d2595be33241cc7b694ae69a262cf58d8d8dbb955d7330357572783d6a6f07cf9ebb56553fa9313e977361f6460547cc6f4680d63642f30140d73e27ae
6
+ metadata.gz: 66e8f18cd12134048d4a89d95f4d3c93faeb2f0700e0e31a9ce34b12269feba643c4e896fe2d5b3bc0d36958439e97901b9b2e376fc9afc1c62e455e70af2c66
7
+ data.tar.gz: c983b4b04736ae785e8c3b6c4380968986eaf105326b2d3e10c155f9d86fe0522cd76cb940649685ff11b4d510794828d28e56a737d4e9afda23badceb9262da
@@ -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
@@ -12,9 +12,10 @@ module Yeptris
12
12
  # materializer (Node#to_ruby) stays for node-based use; this is the
13
13
  # Yeptris::YAML fast path.
14
14
  class Materializer
15
- RECORD_SIZE = 36 # YeptrisEventRecord layout (events.h, ABI-pinned)
16
- FIELDS = 12 # unpacked values per record
17
- RECORD_UNPACK = "C4V8" # type/style/flags/tag_id, line..tag_len
15
+ RECORD_SIZE = 44 # YeptrisEventRecord layout (events.h, ABI-pinned;
16
+ # #179 added end_line/end_col in the 0.6.19 window)
17
+ FIELDS = 14 # unpacked values per record
18
+ RECORD_UNPACK = "C4V10" # type/style/flags/tag_id, line..tag_len
18
19
 
19
20
  STREAM_START = 1
20
21
  STREAM_END = 2
@@ -256,10 +257,10 @@ module Yeptris
256
257
  # not the text
257
258
  tag_id = flat[i + 3]
258
259
  v = Materializer.scan_by_tag(
259
- arena[flat[i + 6], flat[i + 7]], tag_id, (flat[i + 2] & EF_IMPLICIT) != 0
260
+ arena[flat[i + 8], flat[i + 9]], tag_id, (flat[i + 2] & EF_IMPLICIT) != 0
260
261
  )
261
- l = flat[i + 9]
262
- anchors[arena[flat[i + 8], l]] = v if l != 0
262
+ l = flat[i + 11]
263
+ anchors[arena[flat[i + 10], l]] = v if l != 0
263
264
  # place(): the scalar fast path inlined — the overwhelming
264
265
  # majority of events land here
265
266
  if stack.empty?
@@ -284,22 +285,22 @@ module Yeptris
284
285
  end
285
286
  when MAPPING_START
286
287
  h = {}
287
- l = flat[i + 9]
288
- anchors[arena[flat[i + 8], l]] = h if l != 0
288
+ l = flat[i + 11]
289
+ anchors[arena[flat[i + 10], l]] = h if l != 0
289
290
  merge_target.push(place(docs, stack, pending_key, pending_key_merge, h))
290
291
  stack.push(h)
291
292
  pending_key.push(nil)
292
293
  pending_key_merge.push(nil)
293
294
  when SEQUENCE_START
294
295
  a = []
295
- l = flat[i + 9]
296
- anchors[arena[flat[i + 8], l]] = a if l != 0
296
+ l = flat[i + 11]
297
+ anchors[arena[flat[i + 10], l]] = a if l != 0
297
298
  merge_target.push(place(docs, stack, pending_key, pending_key_merge, a))
298
299
  stack.push(a)
299
300
  pending_key.push(nil)
300
301
  pending_key_merge.push(nil)
301
302
  when ALIAS
302
- name = arena[flat[i + 6], flat[i + 7]]
303
+ name = arena[flat[i + 8], flat[i + 9]]
303
304
  raise Yeptris::ParseError, "unknown anchor: #{name.inspect}" unless anchors.key?(name)
304
305
 
305
306
  place(docs, stack, pending_key, pending_key_merge, anchors[name])
@@ -49,6 +49,11 @@ module Yeptris
49
49
 
50
50
  def alias(anchor); end
51
51
 
52
+ # #179: called before EVERY event with the event's 0-based marks
53
+ # (psych 5's contract — TreeBuilder stashes them for node
54
+ # locations). No-op by default; handlers that care override it.
55
+ def event_location(start_line, start_column, end_line, end_column); end
56
+
52
57
  # +style+ is one of the scalar constants above; +plain+ /
53
58
  # +quoted+ are the implicit-typing flags (plain: resolvable
54
59
  # without a tag).
@@ -52,12 +52,17 @@ module Yeptris
52
52
  h = @handler
53
53
  i = 0
54
54
  while i < flat.length
55
+ # #179: psych's parser calls event_location before every
56
+ # event; TreeBuilder stashes the marks for node locations
57
+ # (0-based).
58
+ h.event_location(flat[i + 4] - 1, flat[i + 5] - 1,
59
+ flat[i + 6] - 1, flat[i + 7] - 1)
55
60
  type = flat[i]
56
61
  style = flat[i + 1]
57
62
  flags = flat[i + 2]
58
- value = field(flat, arena, i + 6)
59
- anchor = field(flat, arena, i + 8)
60
- tag = field(flat, arena, i + 10)
63
+ value = field(flat, arena, i + 8)
64
+ anchor = field(flat, arena, i + 10)
65
+ tag = field(flat, arena, i + 12)
61
66
  case type
62
67
  when STREAM_START then h.start_stream(UTF8)
63
68
  when STREAM_END then h.end_stream
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ module Psych
5
+ # The stdlib TreeBuilder port (#179): builds a Nodes tree carrying
6
+ # start/end line:column on every node. The parser calls
7
+ # event_location before each event (psych 5's exact contract);
8
+ # stdlib's class is the reference — this port follows it
9
+ # function-for-function over the binding's keyword-arg Nodes.
10
+ class TreeBuilder < Handler
11
+ attr_reader :root
12
+
13
+ def initialize
14
+ super
15
+ @stack = []
16
+ @last = nil
17
+ @root = nil
18
+ @start_line = nil
19
+ @start_column = nil
20
+ @end_line = nil
21
+ @end_column = nil
22
+ end
23
+
24
+ def event_location(start_line, start_column, end_line, end_column)
25
+ @start_line = start_line
26
+ @start_column = start_column
27
+ @end_line = end_line
28
+ @end_column = end_column
29
+ end
30
+
31
+ %w[Sequence Mapping].each do |node|
32
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
33
+ def start_#{node.downcase}(anchor, tag, implicit, style)
34
+ n = Nodes::#{node}.new(anchor: anchor, tag: tag, style: style)
35
+ set_start_location(n)
36
+ @last.children << n
37
+ push n
38
+ end
39
+
40
+ def end_#{node.downcase}
41
+ n = pop
42
+ set_end_location(n)
43
+ n
44
+ end
45
+ RUBY
46
+ end
47
+
48
+ def start_document(version, tag_directives, implicit)
49
+ n = Nodes::Document.new(version, tag_directives)
50
+ set_start_location(n)
51
+ @last.children << n
52
+ push n
53
+ end
54
+
55
+ def end_document(_implicit = false)
56
+ n = pop
57
+ set_end_location(n)
58
+ n
59
+ end
60
+
61
+ def start_stream(_encoding)
62
+ n = Nodes::Stream.new
63
+ set_start_location(n)
64
+ @root = n
65
+ push n
66
+ end
67
+
68
+ def end_stream
69
+ n = pop
70
+ set_end_location(n)
71
+ n
72
+ end
73
+
74
+ def scalar(value, anchor, tag, plain, quoted, style)
75
+ n = Nodes::Scalar.new(value, anchor: anchor, tag: tag,
76
+ plain: plain, quoted: quoted, style: style)
77
+ set_location(n)
78
+ @last.children << n
79
+ n
80
+ end
81
+
82
+ def alias(anchor)
83
+ n = Nodes::Alias.new(anchor)
84
+ set_location(n)
85
+ @last.children << n
86
+ n
87
+ end
88
+
89
+ private
90
+
91
+ def push(value)
92
+ @stack.push(value)
93
+ @last = value
94
+ end
95
+
96
+ def pop
97
+ value = @stack.pop
98
+ @last = @stack.last
99
+ value
100
+ end
101
+
102
+ def set_location(n)
103
+ n.start_line = @start_line
104
+ n.start_column = @start_column
105
+ n.end_line = @end_line
106
+ n.end_column = @end_column
107
+ end
108
+
109
+ def set_start_location(n)
110
+ set_location(n)
111
+ end
112
+
113
+ def set_end_location(n)
114
+ n.end_line = @end_line
115
+ n.end_column = @end_column
116
+ end
117
+ end
118
+ end
119
+ end
data/lib/yeptris/psych.rb CHANGED
@@ -86,6 +86,8 @@ module Yeptris
86
86
  # own entry so referencing Psych::Handlers triggers the load
87
87
  autoload :Handlers, "yeptris/psych/handler"
88
88
  autoload :Parser, "yeptris/psych/parser"
89
+ # #179: the located tree builder (stdlib's class, ported)
90
+ autoload :TreeBuilder, "yeptris/psych/tree_builder"
89
91
  autoload :CoderShim, "yeptris/psych/coder_shim"
90
92
  autoload :Visitors, "yeptris/psych/visitors"
91
93
  # The typed opt-in marker for arbitrary-object dump/load
@@ -425,6 +427,8 @@ module Yeptris
425
427
  include Enumerable
426
428
 
427
429
  attr_reader :children
430
+ # #179: the event marks (0-based), applied by TreeBuilder
431
+ attr_accessor :start_line, :start_column, :end_line, :end_column
428
432
  attr_reader :handle # @api private — the Yeptris::Node
429
433
  # @api private — the tree builder attaches handles; a writer,
430
434
  # never instance_variable_set from outside
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.18.2".freeze
6
+ VERSION = "0.6.20.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
@@ -91,17 +91,21 @@ end
91
91
  # Optional C-API materializer (TODO.restructure/22): fused visit →
92
92
  # Ruby objects via the Ruby C API. Feature-detected — LoadError leaves
93
93
  # 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.
94
+ # One bundle per Ruby minor (the leptris-ruby #207/#227 lesson: a PE
95
+ # DLL must bind its build Ruby's runtime; a unix bundle is also
96
+ # minor-locked by the internal API it compiles against). The MINOR
97
+ # lives in the DIRECTORY (lib/yeptris/<minor>/native.<ext>) and the
98
+ # FILE name stays unversioned: ruby derives the init symbol from the
99
+ # feature basename, so native-3.4.so asked for the untypeable
100
+ # "Init_native-3" and the versioned-file scheme could never load
101
+ # (#157 — every shipped bundle was dead code). Dev checkouts keep
102
+ # the plain native.<ext> name (mkmf layout, built for the running
103
+ # Ruby). A missing cell falls back LOUDLY to the ladder.
98
104
  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+/]}"
105
+ # per-minor FIRST: a single version-agnostic native.so loads under
106
+ # ANY Ruby and is an ABI gamble across minors — Ruby 3.0 ran a
107
+ # 3.3-built ext and the JSON parse failed.
108
+ require "yeptris/#{RUBY_VERSION[/\A\d+\.\d+/]}/native"
105
109
  rescue LoadError
106
110
  begin
107
111
  require "yeptris/native"
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.18
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
@@ -66,6 +66,9 @@ typedef struct {
66
66
  size_t tag_len;
67
67
  uint32_t line; /* 1-based node start */
68
68
  uint32_t col;
69
+ uint32_t end_line; /* #179: libyaml end_mark semantics — 1-based line,
70
+ * col one past the node's end token */
71
+ uint32_t end_col;
69
72
  } YeptrisEvent;
70
73
 
71
74
  /* ---- push ------------------------------------------------------------- */
@@ -106,13 +109,17 @@ typedef struct {
106
109
  * struct's pad byte: sizeof stays 36 (ABI-pinned) */
107
110
  uint32_t line;
108
111
  uint32_t col;
112
+ uint32_t end_line; /* #179: libyaml end_mark semantics (see the
113
+ * internal event) — 1-based, col past the end */
114
+ uint32_t end_col;
109
115
  uint32_t value_off;
110
116
  uint32_t value_len;
111
117
  uint32_t anchor_off;
112
118
  uint32_t anchor_len;
113
119
  uint32_t tag_off;
114
120
  uint32_t tag_len;
115
- } YeptrisEventRecord;
121
+ } YeptrisEventRecord; /* 44B — the bindings mirror this layout in the
122
+ same lockstep window (the 0.6.9 law) */
116
123
 
117
124
  YEPTRIS_API YeptrisRecorder yeptris_recorder_new(void);
118
125
 
@@ -868,6 +868,7 @@ static YeptrisDocument cbor_wrap(yep_dom* dom, const void* buf, const yep_alloca
868
868
  d->input = (const char*)buf;
869
869
  d->finish_pool = NULL;
870
870
  d->lazy_tape = NULL; /* field-by-field ctor: no garbage for free */
871
+ d->lazy_kind = 0;
871
872
  return (YeptrisDocument)d;
872
873
  }
873
874
 
@@ -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 */