parsanol 1.3.76 → 1.3.78

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: 07d32e869bd7ebe22fa7d2055041821da4dd5d3491dacc3c413c4d8271b08a0b
4
- data.tar.gz: 051cdbfae972ba74f9a7a2b4d9bc8fe2cc1f361d363a209178d1f904b2298cfa
3
+ metadata.gz: 8b7c210627fda0c572855efe7f2e27930aff2e01f632de082abeb68dba5a99f7
4
+ data.tar.gz: e6bcb584fe1de6cb62f35273980dfe78e8e24688e76ba089a734606338fe94b6
5
5
  SHA512:
6
- metadata.gz: 0f8bd00470cf0d1ed7341e216c537a6db93116180a448ec3528233b778eb48eadc1b21ab1fb914447807f2706cdb6ee5ed53cb45f1aa4f42e7392f5c1f11c030
7
- data.tar.gz: 05e06ba6641d65ba6542fa5919df3e643d338051671b423f002df5a03e43d954b342b751d2b8b60ac76c7b1813b4db33d6f03af4691d1bbc40298d7005482bc9
6
+ metadata.gz: 49caa14b1f70ecefe02e75e44f572823613f30aabcfcfeb21b9477358cfb20fff09823ff6f2497c90353866de94af3c571072df859ecad702cfb532252547238
7
+ data.tar.gz: 6822401276fc2af955631834129ee5d75ffb64736f76d5719cba1180139d56a778fe8fad599ef59e8ab73cc634190644b4f148c808e1d92ff9065ed47ec05631
data/Cargo.lock CHANGED
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.16.0"
310
+ version = "0.17.1"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "f1d2ab38ca1451e326a8e2f711f4b65f4cde5db3c07f42a1f2c6cc53451a830b"
312
+ checksum = "170442a26c6a0b5ac25fbdb9c249dbaee821a2d364d4a42219c32af11f008b3d"
313
313
  dependencies = [
314
314
  "ahash",
315
315
  "getrandom 0.3.4",
@@ -326,9 +326,9 @@ dependencies = [
326
326
 
327
327
  [[package]]
328
328
  name = "parsanol-derive"
329
- version = "0.16.0"
329
+ version = "0.17.1"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "8bf00288d64331de21d802b98c5fcbb15d916ea4c151b1349b316ae8af8b5678"
331
+ checksum = "c3567bd641f8a18ee4a1e0339d9e1d0bd804764c1d813efc25b8fd7f24ed8ad3"
332
332
  dependencies = [
333
333
  "proc-macro2",
334
334
  "quote",
@@ -12,5 +12,5 @@ crate-type = ["cdylib"]
12
12
  doc = false
13
13
 
14
14
  [dependencies]
15
- # 0.16.0 carries atom-valued capture writes (native scope/continuation)
16
- parsanol = { version = "0.16.0" }
15
+ # 0.17.0 carries LookSpec + the widened opaque writeback
16
+ parsanol = { version = "0.17.1" }
@@ -29,9 +29,10 @@ magnus = { version = "0.9" }
29
29
 
30
30
  # parsanol parser library (crates.io; 0.12 carries the rs#174 native
31
31
  # backtracking fix: dynamic-dependent memoization + outcome cache)
32
- # parsanol parser library (crates.io; 0.16.0 carries atom-valued capture
33
- # writes: the native scope/continuation fix, parsanol-ruby#160)
34
- parsanol = { version = "0.16.0", features = ["ruby"] }
32
+ # parsanol parser library (crates.io; 0.17.0 carries the LookSpec regex
33
+ # lookbehind (rs#163 gap 1) and the widened opaque writeback: the
34
+ # lazy-continuation fix, parsanol-ruby#160)
35
+ parsanol = { version = "0.17.1", features = ["ruby"] }
35
36
 
36
37
  # Logging
37
38
  log = "0.4"
@@ -2,30 +2,60 @@
2
2
 
3
3
  module Parsanol
4
4
  module Atoms
5
- # Inspects the byte window behind the current position: the
6
- # previous +count+ bytes must equal +pattern+ (positive) or
7
- # differ (negative). Consumes nothing, yields nil — the
8
- # precedes?/does_not_precede? guard shape (coradoc-markdown
9
- # parity, rs#137 follow-up).
5
+ # Inspects the text behind the current position and consumes
6
+ # nothing, yielding nil.
7
+ #
8
+ # Two forms (rs#137/#163):
9
+ # - Literal: the previous +count+ bytes must equal +pattern+
10
+ # (the precedes?/does_not_precede? fixed-window guard).
11
+ # - Regex: +source+ is searched in the preceding text and must
12
+ # end at the position — the CommonMark flanking form, which is
13
+ # class-based, multibyte and variable-length.
10
14
  class Lookbehind < Base
11
- attr_reader :count, :pattern, :positive
15
+ attr_reader :count, :pattern, :regex_source, :positive
12
16
 
13
17
  def initialize(count, pattern, positive: true)
14
18
  super()
15
19
  @count = count
16
20
  @pattern = pattern
21
+ @regex_source = nil
22
+ @compiled = nil
17
23
  @positive = positive
18
24
  end
19
25
 
26
+ # Class-based flanking guard: +source+ must match the text
27
+ # ending at the position.
28
+ def self.regex(source, positive: true)
29
+ atom = new(0, "", positive: positive)
30
+ atom.instance_variable_set(:@regex_source, source)
31
+ atom
32
+ end
33
+
34
+ def regex?
35
+ !@regex_source.nil?
36
+ end
37
+
20
38
  def try(source, _context, _consume_all)
21
39
  start = source.bytepos
22
- behind = start >= @count ? source.input.byteslice(start - @count, @count) : nil
23
- matched = !behind.nil? && behind == @pattern.b
40
+ if regex?
41
+ @compiled ||= Regexp.new("(?:#{@regex_source})\\z")
42
+ matched = @compiled.match?(source.input.byteslice(0, start))
43
+ else
44
+ behind =
45
+ if start >= @count
46
+ source.input.byteslice(start - @count, @count)
47
+ end
48
+ matched = !behind.nil? && behind == @pattern.b
49
+ end
24
50
  [matched == @positive, nil]
25
51
  end
26
52
 
27
53
  def to_s_inner(_prec)
28
- "lookbehind(#{@count}, #{@pattern.inspect}, #{@positive ? '+' : '-'})"
54
+ if regex?
55
+ "lookbehind(/#{@regex_source}/, #{@positive ? '+' : '-'})"
56
+ else
57
+ "lookbehind(#{@count}, #{@pattern.inspect}, #{@positive ? '+' : '-'})"
58
+ end
29
59
  end
30
60
  end
31
61
  end
@@ -132,6 +132,12 @@ module Parsanol
132
132
  #
133
133
  def invoke_from_rust(callback_id, context)
134
134
  block = @mutex.synchronize { @callbacks[callback_id]&.dig(:block) }
135
+ if ENV["PARSANOL_INVOKE_TRACE"]
136
+ cont = context[:captures][:cont]
137
+ warn "INVOKE id=#{callback_id} pos=#{context[:pos]} " \
138
+ "block=#{context[:captures][:block].inspect} " \
139
+ "cont=#{cont.class}@#{cont.object_id}"
140
+ end
135
141
  return nil unless block
136
142
 
137
143
  # Build DynamicContext from hash
@@ -142,18 +148,33 @@ module Parsanol
142
148
  )
143
149
 
144
150
  # Call the block. Parslet-canonical blocks take two arguments
145
- # (source, context): pass the restricted context for both so
146
- # either convention works (GH-76). Captures are read-only
147
- # snapshots of the engine state; write-dependent dispatch must
148
- # run on mode: :ruby.
151
+ # (source, context). The source is a REAL Parsanol::Source
152
+ # positioned at the dispatch point: blocks that read source
153
+ # state (bytepos, line_and_column — the lazy-continuation
154
+ # guards do) previously got the context wrapper for both
155
+ # args and raised NoMethodError, which the bridge turned
156
+ # into a silent dispatch failure (rs#165). Consumption
157
+ # side-effects stay local to the call: the resolved fragment
158
+ # is the parse.
149
159
  result = if block.arity == 2
150
- block.call(ctx, ctx)
160
+ dispatch_source = Parsanol::Source.new(context[:input])
161
+ dispatch_source.bytepos = context[:pos]
162
+ block.call(dispatch_source, ctx)
151
163
  else
152
164
  block.call(ctx)
153
165
  end
154
166
 
155
167
  return nil unless result
156
168
 
169
+ if ENV["PARSANOL_FRAG_TRACE"]
170
+ begin
171
+ json = result.to_atom_json
172
+ warn "FRAG id=#{callback_id} json=#{json[0, 120]}"
173
+ rescue StandardError => e
174
+ warn "FRAG id=#{callback_id} RAISE #{e.class}: #{e.message[0, 140]}"
175
+ end
176
+ end
177
+
157
178
  # Capture-write contract (GH-80): return the atom and the
158
179
  # post-call captures hash; the Rust bridge diffs it against
159
180
  # the seeded values and propagates the block's writes
@@ -121,6 +121,13 @@ module Parsanol
121
121
  }
122
122
  end
123
123
 
124
+ # Public (rs#166): the VM program cache shares compiled
125
+ # programs across fresh parser instances keyed on this —
126
+ # the same disambiguator the grammar cache uses.
127
+ def public_structure_hash(atom)
128
+ grammar_structure_hash(atom)
129
+ end
130
+
124
131
  private
125
132
 
126
133
  def grammar_cache_key(root_atom)
@@ -103,15 +103,17 @@ module Parsanol
103
103
  # atoms — empty match yielding a wire constant.
104
104
  { "Constant" => { "value" => serialize_constant_value(atom.value) } }
105
105
  when Parsanol::Atoms::Lookbehind
106
- # rs#137 follow-up: the precedes?/does_not_precede?
107
- # guards — inspect the bytes behind the position.
108
- {
109
- "Lookbehind" => {
110
- "count" => atom.count,
111
- "pattern" => atom.pattern,
112
- "positive" => atom.positive,
113
- },
114
- }
106
+ # rs#137/#163: the behind-window test — a fixed
107
+ # literal window or an end-anchored regex (the
108
+ # CommonMark flanking form).
109
+ look = if atom.regex?
110
+ { "Regex" => { "source" => atom.regex_source } }
111
+ else
112
+ { "Literal" => { "count" => atom.count,
113
+ "pattern" => atom.pattern } }
114
+ end
115
+ { "Lookbehind" => { "look" => look,
116
+ "positive" => atom.positive } }
115
117
  when Parsanol::Atoms::Ignored
116
118
  serialize_ignored(atom)
117
119
  else
@@ -65,8 +65,16 @@ module Parsanol
65
65
  def parse(entry_name, input, mode: :native)
66
66
  case mode
67
67
  when :native
68
- if !envelope["dynamic"] && Native.available?
69
- return Native.parse(JSON.generate(entry(entry_name).fetch("grammar")), input)
68
+ if Native.available? && !envelope["dynamic"]
69
+ # Fast lane (parsanol-ruby#162): the per-instance atom
70
+ # tree rides the DSL serialization/registration caches
71
+ # (GRAMMAR_CACHE / HANDLE_CACHE keyed on the stable root
72
+ # atom), so an artifact parses at compiled speed after
73
+ # the first call. The old path re-serialized the whole
74
+ # grammar JSON and re-registered it per parse (~8x).
75
+ # Dynamic artifacts carry ruby-tier atoms their wire
76
+ # cannot express — they keep the interpreter.
77
+ return root_atom(entry_name).parse(input, mode: :native)
70
78
  end
71
79
 
72
80
  root_atom(entry_name).parse(input)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.76"
4
+ VERSION = "1.3.78"
5
5
  end
data/lib/parsanol/vm.rb CHANGED
@@ -202,6 +202,10 @@ module Parsanol
202
202
  # starts memoized on its very first parse (fresh parser instances
203
203
  # included — the seed is recomputed at every compile), so the
204
204
  # cold-start never pays the doomed unmemoized exploration.
205
+ # rs#166: proneness rides the structure-hash share as a bool —
206
+ # a later identical-structure grammar inherits the seed instead
207
+ # of running its cold start unmemoized because compilation was
208
+ # skipped entirely.
205
209
  if compiler.backtracking_prone
206
210
  (@heavy ||= {}.compare_by_identity)[root] = true
207
211
  trim_cache(@heavy)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.76
4
+ version: 1.3.78
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.