parsanol 1.3.54 → 1.3.57

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +97 -23
  3. data/exe/parsanol +7 -0
  4. data/ext/parsanol_cdylib/Cargo.toml +1 -1
  5. data/ext/parsanol_native/Cargo.toml +2 -2
  6. data/ext/parsanol_native/build.rs +9 -0
  7. data/ext/parsanol_native/extconf.rb +10 -0
  8. data/lib/parsanol/atoms/base.rb +1 -1
  9. data/lib/parsanol/atoms/cut.rb +1 -1
  10. data/lib/parsanol/atoms/repetition.rb +13 -2
  11. data/lib/parsanol/cause.rb +1 -1
  12. data/lib/parsanol/error_reporter/contextual.rb +1 -1
  13. data/lib/parsanol/lazy_result.rb +12 -3
  14. data/lib/parsanol/native/serializer.rb +5 -2
  15. data/lib/parsanol/native/transformer.rb +12 -8
  16. data/lib/parsanol/native.rb +4 -2
  17. data/lib/parsanol/parg/artifact.rb +213 -0
  18. data/lib/parsanol/parg/authoring.rb +110 -0
  19. data/lib/parsanol/parg/bindings.rb +129 -0
  20. data/lib/parsanol/parg/cli.rb +251 -0
  21. data/lib/parsanol/parg/compiler.rb +465 -0
  22. data/lib/parsanol/parg/derive.rb +28 -0
  23. data/lib/parsanol/parg/document.rb +69 -0
  24. data/lib/parsanol/parg/error.rb +18 -0
  25. data/lib/parsanol/parg/frontend.rb +173 -0
  26. data/lib/parsanol/parg/import.rb +34 -0
  27. data/lib/parsanol/parg/importers/abnf.rb +299 -0
  28. data/lib/parsanol/parg/importers/ebnf.rb +201 -0
  29. data/lib/parsanol/parg/importers/pest.rb +314 -0
  30. data/lib/parsanol/parg/imports.rb +87 -0
  31. data/lib/parsanol/parg/lexer.rb +72 -0
  32. data/lib/parsanol/parg/lints.rb +171 -0
  33. data/lib/parsanol/parg/lsp.rb +208 -0
  34. data/lib/parsanol/parg/lutaml.rb +67 -0
  35. data/lib/parsanol/parg/node.rb +20 -0
  36. data/lib/parsanol/parg/parser.rb +458 -0
  37. data/lib/parsanol/parg/preprocess.rb +36 -0
  38. data/lib/parsanol/parg/render.rb +44 -0
  39. data/lib/parsanol/parg/selfhost.rb +49 -0
  40. data/lib/parsanol/parg/visitor.rb +64 -0
  41. data/lib/parsanol/parg.rb +38 -0
  42. data/lib/parsanol/parser.rb +1 -1
  43. data/lib/parsanol/pattern.rb +1 -1
  44. data/lib/parsanol/pool.rb +1 -1
  45. data/lib/parsanol/rope.rb +1 -1
  46. data/lib/parsanol/slice.rb +4 -6
  47. data/lib/parsanol/source/line_cache.rb +1 -1
  48. data/lib/parsanol/source.rb +2 -2
  49. data/lib/parsanol/version.rb +1 -1
  50. data/lib/parsanol/vm.rb +95 -5
  51. data/lib/parsanol.rb +9 -0
  52. data/parsanol.gemspec +3 -1
  53. metadata +31 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea29107bfe3e1b3ce6b66701239beffe98c16a2cb7207da64e81b43ea179544d
4
- data.tar.gz: 6c31ecaaa82edb959a17b8a43ce5ac7d3fa3819c175e28637efac3a35e215760
3
+ metadata.gz: dac1ca6df26172acdc4f7a676c018d1a9cf6107f5a2b6643b1274cbd56ba3b8f
4
+ data.tar.gz: bbeebb38922a236a170578538858e2f795e9da94679dd6c7a0cd81576d4542a7
5
5
  SHA512:
6
- metadata.gz: f4e3f11ff5bf640b96c2ed79bee2db2d2ef3d68efc409a8da5acb684c9b366d4af902ededc6f6e115163a491c27297027c8d5c9fd10975d43b458ccea2b3548a
7
- data.tar.gz: f5ae7316b64a7979558eed0d592e13e3cb5edb79644d1d90c9f997bb34689e71075022f4e8b89dc0c86d5c8e678997eae532e086f36e1bd20c5e70f3408fb9aa
6
+ metadata.gz: 9170d4b42f82853d41741475abb92fe26f126fde281b4c1b2f31b6b6004dab21fa7a9a9a23b580b83c86f89978dd021767d5464c79080ef19d9ba68771cd7d23
7
+ data.tar.gz: 633a3b087e6b6d6656e8ddd0db3c0548f6985ea72e43da51537a95d07c480957397b5fe6eda56b8651a2d302cb6b886b5661fc2446dfc4aceb2943a9c31ba2a5
data/Cargo.lock CHANGED
@@ -54,6 +54,15 @@ version = "2.13.2"
54
54
  source = "registry+https://github.com/rust-lang/crates.io-index"
55
55
  checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
56
56
 
57
+ [[package]]
58
+ name = "block-buffer"
59
+ version = "0.12.1"
60
+ source = "registry+https://github.com/rust-lang/crates.io-index"
61
+ checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
62
+ dependencies = [
63
+ "hybrid-array",
64
+ ]
65
+
57
66
  [[package]]
58
67
  name = "bumpalo"
59
68
  version = "3.20.3"
@@ -86,6 +95,41 @@ dependencies = [
86
95
  "libloading",
87
96
  ]
88
97
 
98
+ [[package]]
99
+ name = "const-oid"
100
+ version = "0.10.2"
101
+ source = "registry+https://github.com/rust-lang/crates.io-index"
102
+ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
103
+
104
+ [[package]]
105
+ name = "cpufeatures"
106
+ version = "0.3.1"
107
+ source = "registry+https://github.com/rust-lang/crates.io-index"
108
+ checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
109
+ dependencies = [
110
+ "libc",
111
+ ]
112
+
113
+ [[package]]
114
+ name = "crypto-common"
115
+ version = "0.2.2"
116
+ source = "registry+https://github.com/rust-lang/crates.io-index"
117
+ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
118
+ dependencies = [
119
+ "hybrid-array",
120
+ ]
121
+
122
+ [[package]]
123
+ name = "digest"
124
+ version = "0.11.3"
125
+ source = "registry+https://github.com/rust-lang/crates.io-index"
126
+ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
127
+ dependencies = [
128
+ "block-buffer",
129
+ "const-oid",
130
+ "crypto-common",
131
+ ]
132
+
89
133
  [[package]]
90
134
  name = "either"
91
135
  version = "1.18.0"
@@ -148,6 +192,15 @@ dependencies = [
148
192
  "foldhash",
149
193
  ]
150
194
 
195
+ [[package]]
196
+ name = "hybrid-array"
197
+ version = "0.4.15"
198
+ source = "registry+https://github.com/rust-lang/crates.io-index"
199
+ checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
200
+ dependencies = [
201
+ "typenum",
202
+ ]
203
+
151
204
  [[package]]
152
205
  name = "itertools"
153
206
  version = "0.13.0"
@@ -165,9 +218,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
165
218
 
166
219
  [[package]]
167
220
  name = "js-sys"
168
- version = "0.3.105"
221
+ version = "0.3.106"
169
222
  source = "registry+https://github.com/rust-lang/crates.io-index"
170
- checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
223
+ checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
171
224
  dependencies = [
172
225
  "cfg-if",
173
226
  "wasm-bindgen",
@@ -203,8 +256,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
203
256
 
204
257
  [[package]]
205
258
  name = "magnus"
206
- version = "0.9.0"
207
- source = "git+https://github.com/matsadler/magnus?rev=9407bd9#9407bd98afef6692edca706426c15ea6bef60d0e"
259
+ version = "0.9.1"
260
+ source = "registry+https://github.com/rust-lang/crates.io-index"
261
+ checksum = "4c01bef153b8d0f086991f0e166474a1d57b738221f9a9c11405d498ac95a44d"
208
262
  dependencies = [
209
263
  "magnus-macros",
210
264
  "rb-sys",
@@ -214,8 +268,9 @@ dependencies = [
214
268
 
215
269
  [[package]]
216
270
  name = "magnus-macros"
217
- version = "0.9.0"
218
- source = "git+https://github.com/matsadler/magnus?rev=9407bd9#9407bd98afef6692edca706426c15ea6bef60d0e"
271
+ version = "0.9.1"
272
+ source = "registry+https://github.com/rust-lang/crates.io-index"
273
+ checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
219
274
  dependencies = [
220
275
  "proc-macro2",
221
276
  "quote",
@@ -252,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
252
307
 
253
308
  [[package]]
254
309
  name = "parsanol"
255
- version = "0.8.8"
310
+ version = "0.10.1"
256
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
257
- checksum = "5709d5bc971fa4e28fccf401709ab1d275d180e447e3ab82fde7ebef2ecc787b"
312
+ checksum = "3201f3b6340113ce57871a1405679ad7520a420cb8958777983371a2d4e6bf55"
258
313
  dependencies = [
259
314
  "ahash",
260
315
  "getrandom 0.3.4",
@@ -263,15 +318,17 @@ dependencies = [
263
318
  "magnus",
264
319
  "parsanol-derive",
265
320
  "regex",
321
+ "regex-syntax",
266
322
  "serde",
267
323
  "serde_json",
324
+ "sha2",
268
325
  ]
269
326
 
270
327
  [[package]]
271
328
  name = "parsanol-derive"
272
- version = "0.8.8"
329
+ version = "0.10.1"
273
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
274
- checksum = "93258404b7032b85dffd9a60e3fb1ed003a268282b4f3385f1ab1aba98db34e5"
331
+ checksum = "07bea1e5fb6a78314596116ac62e08ab5c73c201c62cd83715e75309c44e0977"
275
332
  dependencies = [
276
333
  "proc-macro2",
277
334
  "quote",
@@ -280,7 +337,7 @@ dependencies = [
280
337
 
281
338
  [[package]]
282
339
  name = "parsanol_cdylib"
283
- version = "1.3.50"
340
+ version = "1.3.52"
284
341
  dependencies = [
285
342
  "parsanol",
286
343
  ]
@@ -443,6 +500,17 @@ dependencies = [
443
500
  "zmij",
444
501
  ]
445
502
 
503
+ [[package]]
504
+ name = "sha2"
505
+ version = "0.11.0"
506
+ source = "registry+https://github.com/rust-lang/crates.io-index"
507
+ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
508
+ dependencies = [
509
+ "cfg-if",
510
+ "cpufeatures",
511
+ "digest",
512
+ ]
513
+
446
514
  [[package]]
447
515
  name = "shell-words"
448
516
  version = "1.1.1"
@@ -477,6 +545,12 @@ dependencies = [
477
545
  "unicode-ident",
478
546
  ]
479
547
 
548
+ [[package]]
549
+ name = "typenum"
550
+ version = "1.20.1"
551
+ source = "registry+https://github.com/rust-lang/crates.io-index"
552
+ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
553
+
480
554
  [[package]]
481
555
  name = "unicode-ident"
482
556
  version = "1.0.26"
@@ -500,9 +574,9 @@ dependencies = [
500
574
 
501
575
  [[package]]
502
576
  name = "wasm-bindgen"
503
- version = "0.2.128"
577
+ version = "0.2.129"
504
578
  source = "registry+https://github.com/rust-lang/crates.io-index"
505
- checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
579
+ checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
506
580
  dependencies = [
507
581
  "cfg-if",
508
582
  "once_cell",
@@ -513,9 +587,9 @@ dependencies = [
513
587
 
514
588
  [[package]]
515
589
  name = "wasm-bindgen-macro"
516
- version = "0.2.128"
590
+ version = "0.2.129"
517
591
  source = "registry+https://github.com/rust-lang/crates.io-index"
518
- checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
592
+ checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
519
593
  dependencies = [
520
594
  "quote",
521
595
  "wasm-bindgen-macro-support",
@@ -523,9 +597,9 @@ dependencies = [
523
597
 
524
598
  [[package]]
525
599
  name = "wasm-bindgen-macro-support"
526
- version = "0.2.128"
600
+ version = "0.2.129"
527
601
  source = "registry+https://github.com/rust-lang/crates.io-index"
528
- checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
602
+ checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
529
603
  dependencies = [
530
604
  "bumpalo",
531
605
  "proc-macro2",
@@ -536,9 +610,9 @@ dependencies = [
536
610
 
537
611
  [[package]]
538
612
  name = "wasm-bindgen-shared"
539
- version = "0.2.128"
613
+ version = "0.2.129"
540
614
  source = "registry+https://github.com/rust-lang/crates.io-index"
541
- checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
615
+ checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
542
616
  dependencies = [
543
617
  "unicode-ident",
544
618
  ]
@@ -557,18 +631,18 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
557
631
 
558
632
  [[package]]
559
633
  name = "zerocopy"
560
- version = "0.8.57"
634
+ version = "0.8.59"
561
635
  source = "registry+https://github.com/rust-lang/crates.io-index"
562
- checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
636
+ checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
563
637
  dependencies = [
564
638
  "zerocopy-derive",
565
639
  ]
566
640
 
567
641
  [[package]]
568
642
  name = "zerocopy-derive"
569
- version = "0.8.57"
643
+ version = "0.8.59"
570
644
  source = "registry+https://github.com/rust-lang/crates.io-index"
571
- checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
645
+ checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
572
646
  dependencies = [
573
647
  "proc-macro2",
574
648
  "quote",
data/exe/parsanol ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "parsanol"
5
+ require "parsanol/parg/cli"
6
+ argv = ARGV[0] == "parg" ? ARGV[1..] : ARGV
7
+ exit Parsanol::PARG::CLI.run(argv)
@@ -12,4 +12,4 @@ crate-type = ["cdylib"]
12
12
  doc = false
13
13
 
14
14
  [dependencies]
15
- parsanol = { version = "0.8.8" }
15
+ parsanol = { version = "0.10" }
@@ -27,8 +27,8 @@ rb-sys = { version = "0.9.124", features = ["global-allocator"] }
27
27
  # magnus for Ruby bindings (from crates.io, patched locally for Ruby 4.0 support)
28
28
  magnus = { version = "0.9" }
29
29
 
30
- # parsanol parser library (from git for latest features)
31
- parsanol = { version = "0.8.9", features = ["ruby"] }
30
+ # parsanol parser library (crates.io; 0.10 carries the class fast-path fix)
31
+ parsanol = { version = "0.10", features = ["ruby"] }
32
32
 
33
33
  # Logging
34
34
  log = "0.4"
@@ -0,0 +1,9 @@
1
+ fn main() {
2
+ // MRI loadables on macOS resolve ruby symbols from the host process
3
+ // at load time; cargo's cdylib link defaults to failing on undefined
4
+ // symbols, so the flag must come from the top-level package.
5
+ if cfg!(target_os = "macos") {
6
+ println!("cargo:rustc-link-arg=-Wl,-undefined,dynamic_lookup");
7
+ }
8
+ println!("cargo:rerun-if-changed=build.rs");
9
+ }
@@ -30,6 +30,16 @@ create_rust_makefile("parsanol/parsanol_native") do |r|
30
30
  # Create debug builds in dev, release in production
31
31
  r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :dev).to_sym
32
32
 
33
+ # cargo rustc builds dependency cdylibs verbatim, and parsanol ships
34
+ # a cdylib crate-type (the ffi tier). On macOS every dylib in the
35
+ # build resolves ruby symbols from the host process at load time, so
36
+ # every link must tolerate undefined symbols. (extconf runs as a
37
+ # subprocess, so this must be baked into the Makefile — env changes
38
+ # here would not survive it.)
39
+ if RbConfig::CONFIG["host_os"].include?("darwin")
40
+ r.extra_rustflags = %w[-C link-arg=-Wl,-undefined,dynamic_lookup]
41
+ end
42
+
33
43
  # Enable stable API compiled fallback for ruby-head and older Ruby versions
34
44
  r.use_stable_api_compiled_fallback = true
35
45
 
@@ -208,7 +208,7 @@ module Parsanol
208
208
 
209
209
  # Converts raw input to Source if needed.
210
210
  def normalize_input(source)
211
- source.respond_to?(:line_and_column) ? source : Parsanol::Source.new(source)
211
+ source.is_a?(Parsanol::Source) ? source : Parsanol::Source.new(source)
212
212
  end
213
213
 
214
214
  # Detects if we're in a Parser context.
@@ -42,7 +42,7 @@ module Parsanol
42
42
  # 1. Mark the current position as a cut point
43
43
  # 2. Empty the backtrack stack (we won't backtrack past here)
44
44
  # 3. Aggressively evict cache entries before this position
45
- context.cut!(source.bytepos) if context.respond_to?(:cut!)
45
+ context.cut!(source.bytepos)
46
46
 
47
47
  [success, value]
48
48
  end
@@ -66,7 +66,7 @@ module Parsanol
66
66
  # @return [Array(Boolean, Object)] result
67
67
  def try(source, context, consume_all)
68
68
  # Check for tree memoization support
69
- if context.respond_to?(:use_tree_memoization?) && context.use_tree_memoization?
69
+ if context.use_tree_memoization?
70
70
  return with_tree_cache(source, context, consume_all)
71
71
  end
72
72
 
@@ -183,6 +183,7 @@ module Parsanol
183
183
  last_failure = nil
184
184
 
185
185
  loop do
186
+ iteration_start = source.bytepos
186
187
  success, value = @parslet.apply(source, context, false)
187
188
 
188
189
  unless success
@@ -193,6 +194,11 @@ module Parsanol
193
194
  occurrence += 1
194
195
  result[occurrence] = value
195
196
 
197
+ # A body match that consumes nothing cannot make progress;
198
+ # looping again would re-match empty forever. Count the empty
199
+ # match, then stop — the min check below applies as usual.
200
+ break if source.bytepos == iteration_start
201
+
196
202
  break if @max && occurrence >= @max
197
203
  end
198
204
 
@@ -254,6 +260,7 @@ module Parsanol
254
260
  last_failure = nil
255
261
 
256
262
  loop do
263
+ iteration_start = source.bytepos
257
264
  success, value = @parslet.apply(source, context, false)
258
265
 
259
266
  unless success
@@ -265,6 +272,10 @@ module Parsanol
265
272
  result[occurrence] = value
266
273
  positions << source.bytepos
267
274
 
275
+ # Zero-width body match: count it, stop iterating (the min
276
+ # check below applies as usual) — mirrors try_general.
277
+ break if source.bytepos == iteration_start
278
+
268
279
  break if @max && occurrence >= @max
269
280
  end
270
281
 
@@ -287,7 +298,7 @@ module Parsanol
287
298
  context.store_tree_memo(cache_key, start_pos,
288
299
  result[1, occurrence], end_pos)
289
300
  end
290
- context.release_array(positions) if context.respond_to?(:release_array)
301
+ context.release_array(positions)
291
302
 
292
303
  # Check minimum
293
304
  if occurrence < @min
@@ -74,7 +74,7 @@ module Parsanol
74
74
  line_num, col_num = @source.line_and_column(@position)
75
75
 
76
76
  formatted_msg = @message.map do |msg|
77
- msg.respond_to?(:to_slice) ? msg.content.inspect : msg.to_s
77
+ msg.is_a?(Parsanol::Slice) ? msg.content.inspect : msg.to_s
78
78
  end.join
79
79
 
80
80
  "#{formatted_msg} at line #{line_num} char #{col_num}#{@parsing_label}."
@@ -72,7 +72,7 @@ module Parsanol
72
72
  cause = super
73
73
 
74
74
  # Apply label if the atom has one
75
- if atom.respond_to?(:label) && (lbl = atom.label)
75
+ if atom.is_a?(Parsanol::Atoms::Base) && (lbl = atom.label)
76
76
  maybe_update_label(lbl, src.pos.bytepos)
77
77
  cause.set_label(@active_label_text)
78
78
  end
@@ -103,6 +103,15 @@ module Parsanol
103
103
 
104
104
  alias kind_of? is_a?
105
105
 
106
+ # The delegated interface: Array's full class contract (including
107
+ # Enumerable), computed once against the type rather than probed on
108
+ # instances.
109
+ ARRAY_METHODS = (
110
+ Array.instance_methods(true) + Enumerable.instance_methods + [:to_ary]
111
+ ).freeze
112
+
113
+ private_constant :ARRAY_METHODS
114
+
106
115
  # Respond to array methods.
107
116
  #
108
117
  # @param method [Symbol] Method name
@@ -110,7 +119,7 @@ module Parsanol
110
119
  # @return [Boolean] true if responds
111
120
  #
112
121
  def respond_to?(method, include_private = false)
113
- super || to_a.respond_to?(method, include_private)
122
+ super || ARRAY_METHODS.include?(method)
114
123
  end
115
124
 
116
125
  # Delegate unknown methods to materialized array.
@@ -121,7 +130,7 @@ module Parsanol
121
130
  # @return [Object] Result of method call
122
131
  #
123
132
  def method_missing(method, ...)
124
- if to_a.respond_to?(method)
133
+ if ARRAY_METHODS.include?(method)
125
134
  to_a.public_send(method, ...)
126
135
  else
127
136
  super
@@ -135,7 +144,7 @@ module Parsanol
135
144
  # @return [Boolean] true if method is supported
136
145
  #
137
146
  def respond_to_missing?(method, include_private = false)
138
- to_a.respond_to?(method, include_private) || super
147
+ ARRAY_METHODS.include?(method) || super
139
148
  end
140
149
 
141
150
  # Compare with another object.
@@ -95,9 +95,12 @@ module Parsanol
95
95
 
96
96
  def serialize_re(atom)
97
97
  # Ruby's Regexp#to_s produces "(?-mix:pattern)" format
98
- # We need to extract just the pattern for the Rust parser
98
+ # The pattern must reach the Rust regex engine verbatim, including
99
+ # inline flag groups: stripping (?i:...) silently turned every
100
+ # case-insensitive regex into a case-sensitive one on the native
101
+ # path (the Ruby atom still compiled it with the flag, so the two
102
+ # engines disagreed). Rust regex understands the same inline groups.
99
103
  pattern = atom.match
100
- pattern = ::Regexp.last_match(1) if pattern =~ /^\(\?[-mix]*:(.+)\)$/
101
104
  {
102
105
  "Re" => {
103
106
  "pattern" => pattern,
@@ -391,15 +391,19 @@ module Parsanol
391
391
  return left.is_a?(Hash) ? left.merge(right) : left + right
392
392
  end
393
393
 
394
- if left.respond_to?(:to_str) && right.respond_to?(:to_str)
395
- return right if right.respond_to?(:to_slice)
396
- return left if left.respond_to?(:to_slice)
397
-
398
- return left.to_str + right.to_str
394
+ # String-like = String or Slice (both implement to_str). When
395
+ # only ONE side is string-like, the OTHER side wins — the
396
+ # original duck-typed priority, preserved exactly.
397
+ left_stringlike = left.is_a?(String) || left.is_a?(Parsanol::Slice)
398
+ right_stringlike = right.is_a?(String) || right.is_a?(Parsanol::Slice)
399
+ if left_stringlike && right_stringlike
400
+ return right if right.is_a?(Parsanol::Slice)
401
+ return left if left.is_a?(Parsanol::Slice)
402
+
403
+ return left + right
399
404
  end
400
-
401
- return left if right.respond_to?(:to_str)
402
- return right if left.respond_to?(:to_str)
405
+ return left if right_stringlike
406
+ return right if left_stringlike
403
407
 
404
408
  return left + [right] if right.is_a?(Hash)
405
409
  return [left] + right if left.is_a?(Hash)
@@ -216,7 +216,9 @@ module Parsanol
216
216
  # cause from that directly — the failure path never needs a
217
217
  # reporter reparse. The single interpreter pass below stays only
218
218
  # to recover inputs from grammars native cannot express.
219
- if grammar.respond_to?(:parse) && (m = error.message.match(NATIVE_POS_MARKER))
219
+ parsanol_grammar =
220
+ grammar.is_a?(Parsanol::Atoms::Base) || grammar.is_a?(Parsanol::Parser)
221
+ if parsanol_grammar && (m = error.message.match(NATIVE_POS_MARKER))
220
222
  source = Parsanol::Source.new(input)
221
223
  success, value = grammar.run_with_context(source, nil, true)
222
224
  return grammar.finalize_result(value) if success
@@ -227,7 +229,7 @@ module Parsanol
227
229
  raise Parsanol::ParseFailed.new(cause.to_s, cause)
228
230
  end
229
231
 
230
- if grammar.respond_to?(:parse)
232
+ if parsanol_grammar
231
233
  # No native diagnostics (e.g. incomplete-input errors): one
232
234
  # interpreter pass with the reporter attached — a success
233
235
  # recovers the tree, a failure raises the cause tree.