parsanol 3.0.0

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.

Potentially problematic release.


This version of parsanol might be problematic. Click here for more details.

Files changed (336) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +25 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +643 -0
  5. data/Rakefile +189 -0
  6. data/example/balanced-parens/basic.rb +42 -0
  7. data/example/balanced-parens/basic.rb.md +86 -0
  8. data/example/balanced-parens/parens.rb +42 -0
  9. data/example/balanced-parens/ruby_transform.rb +162 -0
  10. data/example/big.erb +73 -0
  11. data/example/boolean-algebra/basic.rb +70 -0
  12. data/example/boolean-algebra/basic.rb.md +108 -0
  13. data/example/boolean-algebra/ruby_transform.rb +263 -0
  14. data/example/calculator/basic.rb +153 -0
  15. data/example/calculator/basic.rb.md +120 -0
  16. data/example/calculator/pattern.rb +153 -0
  17. data/example/calculator/ruby_transform.rb +156 -0
  18. data/example/calculator/ruby_transform.rb.md +32 -0
  19. data/example/calculator/serialized.rb +257 -0
  20. data/example/calculator/serialized.rb.md +32 -0
  21. data/example/calculator/transform.rb +153 -0
  22. data/example/calculator/zero_copy.rb +269 -0
  23. data/example/calculator/zero_copy.rb.md +36 -0
  24. data/example/capture/basic.rb +49 -0
  25. data/example/capture/basic.rb.md +106 -0
  26. data/example/capture/example.json +39 -0
  27. data/example/comments/basic.rb +35 -0
  28. data/example/comments/basic.rb.md +110 -0
  29. data/example/csv/ruby_transform.rb +148 -0
  30. data/example/csv/ruby_transform.rb.md +131 -0
  31. data/example/csv/serialized.rb +201 -0
  32. data/example/csv/serialized.rb.md +31 -0
  33. data/example/csv/zero_copy.rb +276 -0
  34. data/example/csv/zero_copy.rb.md +36 -0
  35. data/example/custom_atoms/indent_atom.rb +79 -0
  36. data/example/deepest-errors/basic.rb +131 -0
  37. data/example/deepest-errors/basic.rb.md +152 -0
  38. data/example/documentation/basic.rb +18 -0
  39. data/example/documentation/basic.rb.md +97 -0
  40. data/example/email/basic.rb +55 -0
  41. data/example/email/basic.rb.md +102 -0
  42. data/example/email/ruby_transform.rb +106 -0
  43. data/example/empty/basic.rb +13 -0
  44. data/example/empty/basic.rb.md +73 -0
  45. data/example/empty/example.json +38 -0
  46. data/example/erb/basic.rb +47 -0
  47. data/example/erb/basic.rb.md +103 -0
  48. data/example/erb/optimized.rb +42 -0
  49. data/example/error-reporting/basic.rb +132 -0
  50. data/example/error-reporting/basic.rb.md +122 -0
  51. data/example/expression-evaluator/basic.rb +284 -0
  52. data/example/expression-evaluator/basic.rb.md +138 -0
  53. data/example/ini/basic.rb +154 -0
  54. data/example/ini/basic.rb.md +129 -0
  55. data/example/ini/ruby_transform.rb +154 -0
  56. data/example/ip-address/basic.rb +125 -0
  57. data/example/ip-address/basic.rb.md +139 -0
  58. data/example/iso-6709/basic.rb +231 -0
  59. data/example/iso-6709/basic.rb.md +143 -0
  60. data/example/iso-8601/basic.rb +275 -0
  61. data/example/iso-8601/basic.rb.md +149 -0
  62. data/example/json/basic.rb +128 -0
  63. data/example/json/basic.rb.md +121 -0
  64. data/example/json/pattern.rb +128 -0
  65. data/example/json/ruby_transform.rb +200 -0
  66. data/example/json/ruby_transform.rb.md +32 -0
  67. data/example/json/serialized.rb +233 -0
  68. data/example/json/serialized.rb.md +31 -0
  69. data/example/json/transform.rb +128 -0
  70. data/example/json/zero_copy.rb +316 -0
  71. data/example/json/zero_copy.rb.md +36 -0
  72. data/example/local/basic.rb +34 -0
  73. data/example/local/basic.rb.md +91 -0
  74. data/example/local/example.json +38 -0
  75. data/example/markdown/basic.rb +287 -0
  76. data/example/markdown/basic.rb.md +160 -0
  77. data/example/markup/basic.rb +173 -0
  78. data/example/markup/basic.rb.md +118 -0
  79. data/example/mathn/basic.rb +47 -0
  80. data/example/mathn/basic.rb.md +96 -0
  81. data/example/mathn/example.json +39 -0
  82. data/example/minilisp/basic.rb +94 -0
  83. data/example/minilisp/basic.rb.md +133 -0
  84. data/example/modularity/basic.rb +47 -0
  85. data/example/modularity/basic.rb.md +152 -0
  86. data/example/nested-errors/basic.rb +132 -0
  87. data/example/nested-errors/basic.rb.md +157 -0
  88. data/example/output/boolean_algebra.out +4 -0
  89. data/example/output/calc.out +1 -0
  90. data/example/output/capture.out +3 -0
  91. data/example/output/comments.out +8 -0
  92. data/example/output/deepest_errors.out +54 -0
  93. data/example/output/documentation.err +4 -0
  94. data/example/output/documentation.out +1 -0
  95. data/example/output/email_parser.out +2 -0
  96. data/example/output/empty.err +1 -0
  97. data/example/output/erb.out +7 -0
  98. data/example/output/ignore.out +1 -0
  99. data/example/output/ignore_whitespace.out +1 -0
  100. data/example/output/ip_address.out +9 -0
  101. data/example/output/json.out +5 -0
  102. data/example/output/local.out +3 -0
  103. data/example/output/mathn.out +4 -0
  104. data/example/output/minilisp.out +5 -0
  105. data/example/output/modularity.out +0 -0
  106. data/example/output/nested_errors.out +54 -0
  107. data/example/output/optimized_erb.out +1 -0
  108. data/example/output/parens.out +8 -0
  109. data/example/output/prec_calc.out +5 -0
  110. data/example/output/readme.out +1 -0
  111. data/example/output/scopes.out +1 -0
  112. data/example/output/seasons.out +28 -0
  113. data/example/output/sentence.out +1 -0
  114. data/example/output/simple_xml.out +2 -0
  115. data/example/output/string_parser.out +3 -0
  116. data/example/prec-calc/basic.rb +71 -0
  117. data/example/prec-calc/basic.rb.md +114 -0
  118. data/example/readme/basic.rb +30 -0
  119. data/example/readme/basic.rb.md +80 -0
  120. data/example/scopes/basic.rb +15 -0
  121. data/example/scopes/basic.rb.md +73 -0
  122. data/example/scopes/example.json +38 -0
  123. data/example/seasons/basic.rb +46 -0
  124. data/example/seasons/basic.rb.md +117 -0
  125. data/example/seasons/example.json +40 -0
  126. data/example/sentence/basic.rb +36 -0
  127. data/example/sentence/basic.rb.md +81 -0
  128. data/example/sexp/ruby_transform.rb +180 -0
  129. data/example/sexp/ruby_transform.rb.md +143 -0
  130. data/example/simple-xml/basic.rb +54 -0
  131. data/example/simple-xml/basic.rb.md +125 -0
  132. data/example/simple.lit +3 -0
  133. data/example/string-literal/basic.rb +77 -0
  134. data/example/string-literal/basic.rb.md +128 -0
  135. data/example/test.lit +4 -0
  136. data/example/toml/basic.rb +226 -0
  137. data/example/toml/basic.rb.md +173 -0
  138. data/example/url/basic.rb +219 -0
  139. data/example/url/basic.rb.md +142 -0
  140. data/example/url/ruby_transform.rb +219 -0
  141. data/example/yaml/basic.rb +216 -0
  142. data/example/yaml/basic.rb.md +148 -0
  143. data/ext/parsanol_native/extconf.rb +4 -0
  144. data/lib/parsanol/accelerator/application.rb +62 -0
  145. data/lib/parsanol/accelerator/engine.rb +112 -0
  146. data/lib/parsanol/accelerator.rb +162 -0
  147. data/lib/parsanol/ast_visitor.rb +122 -0
  148. data/lib/parsanol/atoms/alternative.rb +97 -0
  149. data/lib/parsanol/atoms/base.rb +214 -0
  150. data/lib/parsanol/atoms/can_flatten.rb +192 -0
  151. data/lib/parsanol/atoms/capture.rb +41 -0
  152. data/lib/parsanol/atoms/context.rb +351 -0
  153. data/lib/parsanol/atoms/context_optimized.rb +42 -0
  154. data/lib/parsanol/atoms/custom.rb +110 -0
  155. data/lib/parsanol/atoms/cut.rb +62 -0
  156. data/lib/parsanol/atoms/dsl.rb +130 -0
  157. data/lib/parsanol/atoms/dynamic.rb +33 -0
  158. data/lib/parsanol/atoms/entity.rb +55 -0
  159. data/lib/parsanol/atoms/ignored.rb +28 -0
  160. data/lib/parsanol/atoms/infix.rb +121 -0
  161. data/lib/parsanol/atoms/lookahead.rb +64 -0
  162. data/lib/parsanol/atoms/named.rb +50 -0
  163. data/lib/parsanol/atoms/re.rb +61 -0
  164. data/lib/parsanol/atoms/repetition.rb +241 -0
  165. data/lib/parsanol/atoms/scope.rb +28 -0
  166. data/lib/parsanol/atoms/sequence.rb +157 -0
  167. data/lib/parsanol/atoms/str.rb +90 -0
  168. data/lib/parsanol/atoms/visitor.rb +91 -0
  169. data/lib/parsanol/atoms.rb +36 -0
  170. data/lib/parsanol/buffer.rb +130 -0
  171. data/lib/parsanol/builder_callbacks.rb +353 -0
  172. data/lib/parsanol/cause.rb +101 -0
  173. data/lib/parsanol/context.rb +23 -0
  174. data/lib/parsanol/convenience.rb +35 -0
  175. data/lib/parsanol/edit_tracker.rb +107 -0
  176. data/lib/parsanol/error_reporter/contextual.rb +122 -0
  177. data/lib/parsanol/error_reporter/deepest.rb +106 -0
  178. data/lib/parsanol/error_reporter/tree.rb +68 -0
  179. data/lib/parsanol/error_reporter.rb +98 -0
  180. data/lib/parsanol/export.rb +163 -0
  181. data/lib/parsanol/expression/treetop.rb +94 -0
  182. data/lib/parsanol/expression.rb +51 -0
  183. data/lib/parsanol/fast_mode.rb +145 -0
  184. data/lib/parsanol/first_set.rb +75 -0
  185. data/lib/parsanol/grammar_builder.rb +177 -0
  186. data/lib/parsanol/graphviz.rb +97 -0
  187. data/lib/parsanol/incremental_parser.rb +179 -0
  188. data/lib/parsanol/interval_tree.rb +215 -0
  189. data/lib/parsanol/lazy_result.rb +178 -0
  190. data/lib/parsanol/lexer.rb +146 -0
  191. data/lib/parsanol/native/parser.rb +630 -0
  192. data/lib/parsanol/native/serializer.rb +245 -0
  193. data/lib/parsanol/native/transformer.rb +438 -0
  194. data/lib/parsanol/native/types.rb +41 -0
  195. data/lib/parsanol/native.rb +217 -0
  196. data/lib/parsanol/optimizer.rb +86 -0
  197. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  198. data/lib/parsanol/optimizers/cut_inserter.rb +175 -0
  199. data/lib/parsanol/optimizers/lookahead_optimizer.rb +58 -0
  200. data/lib/parsanol/optimizers/quantifier_optimizer.rb +62 -0
  201. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  202. data/lib/parsanol/options/ruby_transform.rb +109 -0
  203. data/lib/parsanol/options/serialized.rb +94 -0
  204. data/lib/parsanol/options/zero_copy.rb +130 -0
  205. data/lib/parsanol/options.rb +20 -0
  206. data/lib/parsanol/parallel.rb +133 -0
  207. data/lib/parsanol/parsanol_native.bundle +0 -0
  208. data/lib/parsanol/parser.rb +151 -0
  209. data/lib/parsanol/parslet.rb +148 -0
  210. data/lib/parsanol/parslet_native.bundle +0 -0
  211. data/lib/parsanol/pattern/binding.rb +49 -0
  212. data/lib/parsanol/pattern.rb +115 -0
  213. data/lib/parsanol/pool.rb +220 -0
  214. data/lib/parsanol/pools/array_pool.rb +75 -0
  215. data/lib/parsanol/pools/buffer_pool.rb +173 -0
  216. data/lib/parsanol/pools/position_pool.rb +92 -0
  217. data/lib/parsanol/pools/slice_pool.rb +64 -0
  218. data/lib/parsanol/position.rb +89 -0
  219. data/lib/parsanol/result.rb +44 -0
  220. data/lib/parsanol/result_builder.rb +208 -0
  221. data/lib/parsanol/result_stream.rb +262 -0
  222. data/lib/parsanol/rig/rspec.rb +52 -0
  223. data/lib/parsanol/rope.rb +78 -0
  224. data/lib/parsanol/scope.rb +42 -0
  225. data/lib/parsanol/slice.rb +172 -0
  226. data/lib/parsanol/source/line_cache.rb +99 -0
  227. data/lib/parsanol/source.rb +171 -0
  228. data/lib/parsanol/source_location.rb +164 -0
  229. data/lib/parsanol/streaming_parser.rb +124 -0
  230. data/lib/parsanol/string_view.rb +192 -0
  231. data/lib/parsanol/transform.rb +267 -0
  232. data/lib/parsanol/version.rb +5 -0
  233. data/lib/parsanol/wasm/README.md +80 -0
  234. data/lib/parsanol/wasm/package.json +51 -0
  235. data/lib/parsanol/wasm/parsanol.js +252 -0
  236. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  237. data/lib/parsanol/wasm_parser.rb +239 -0
  238. data/lib/parsanol.rb +408 -0
  239. data/parsanol-ruby.gemspec +56 -0
  240. data/spec/acceptance/examples_spec.rb +96 -0
  241. data/spec/acceptance/infix_parser_spec.rb +145 -0
  242. data/spec/acceptance/mixing_parsers_spec.rb +74 -0
  243. data/spec/acceptance/regression_spec.rb +329 -0
  244. data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
  245. data/spec/acceptance/unconsumed_input_spec.rb +21 -0
  246. data/spec/benchmark/comparative/runner_spec.rb +105 -0
  247. data/spec/integration/array_pooling_spec.rb +193 -0
  248. data/spec/integration/buffer_allocation_spec.rb +324 -0
  249. data/spec/integration/position_pooling_spec.rb +184 -0
  250. data/spec/integration/result_builder_spec.rb +282 -0
  251. data/spec/integration/rope_stringview_integration_spec.rb +188 -0
  252. data/spec/integration/slice_pooling_spec.rb +63 -0
  253. data/spec/integration/string_view_integration_spec.rb +125 -0
  254. data/spec/lexer_spec.rb +231 -0
  255. data/spec/parsanol/atom_results_spec.rb +39 -0
  256. data/spec/parsanol/atoms/alternative_spec.rb +26 -0
  257. data/spec/parsanol/atoms/base_spec.rb +127 -0
  258. data/spec/parsanol/atoms/capture_spec.rb +21 -0
  259. data/spec/parsanol/atoms/combinations_spec.rb +5 -0
  260. data/spec/parsanol/atoms/custom_spec.rb +79 -0
  261. data/spec/parsanol/atoms/dsl_spec.rb +7 -0
  262. data/spec/parsanol/atoms/entity_spec.rb +77 -0
  263. data/spec/parsanol/atoms/ignored_spec.rb +15 -0
  264. data/spec/parsanol/atoms/infix_spec.rb +5 -0
  265. data/spec/parsanol/atoms/lookahead_spec.rb +22 -0
  266. data/spec/parsanol/atoms/named_spec.rb +4 -0
  267. data/spec/parsanol/atoms/re_spec.rb +14 -0
  268. data/spec/parsanol/atoms/repetition_spec.rb +24 -0
  269. data/spec/parsanol/atoms/scope_spec.rb +26 -0
  270. data/spec/parsanol/atoms/sequence_spec.rb +28 -0
  271. data/spec/parsanol/atoms/str_spec.rb +15 -0
  272. data/spec/parsanol/atoms/visitor_spec.rb +101 -0
  273. data/spec/parsanol/atoms_spec.rb +488 -0
  274. data/spec/parsanol/auto_optimize_spec.rb +334 -0
  275. data/spec/parsanol/buffer_spec.rb +219 -0
  276. data/spec/parsanol/builder_callbacks_spec.rb +377 -0
  277. data/spec/parsanol/choice_optimizer_spec.rb +231 -0
  278. data/spec/parsanol/convenience_spec.rb +54 -0
  279. data/spec/parsanol/cut_inserter_spec.rb +248 -0
  280. data/spec/parsanol/cut_spec.rb +66 -0
  281. data/spec/parsanol/edit_tracker_spec.rb +218 -0
  282. data/spec/parsanol/error_reporter/contextual_spec.rb +122 -0
  283. data/spec/parsanol/error_reporter/deepest_spec.rb +82 -0
  284. data/spec/parsanol/error_reporter/tree_spec.rb +7 -0
  285. data/spec/parsanol/export_spec.rb +67 -0
  286. data/spec/parsanol/expression/treetop_spec.rb +75 -0
  287. data/spec/parsanol/first_set_spec.rb +298 -0
  288. data/spec/parsanol/interval_tree_spec.rb +205 -0
  289. data/spec/parsanol/lazy_result_spec.rb +288 -0
  290. data/spec/parsanol/lookahead_optimizer_spec.rb +252 -0
  291. data/spec/parsanol/minilisp.citrus +29 -0
  292. data/spec/parsanol/minilisp.tt +29 -0
  293. data/spec/parsanol/optimizer_spec.rb +459 -0
  294. data/spec/parsanol/options/parslet_compat_spec.rb +166 -0
  295. data/spec/parsanol/options/ruby_transform_spec.rb +70 -0
  296. data/spec/parsanol/options/serialized_spec.rb +69 -0
  297. data/spec/parsanol/options/zero_copy_spec.rb +230 -0
  298. data/spec/parsanol/parser_spec.rb +36 -0
  299. data/spec/parsanol/parslet_spec.rb +38 -0
  300. data/spec/parsanol/pattern_spec.rb +272 -0
  301. data/spec/parsanol/pool_spec.rb +392 -0
  302. data/spec/parsanol/pools/array_pool_spec.rb +356 -0
  303. data/spec/parsanol/pools/buffer_pool_spec.rb +365 -0
  304. data/spec/parsanol/pools/position_pool_spec.rb +118 -0
  305. data/spec/parsanol/pools/slice_pool_spec.rb +262 -0
  306. data/spec/parsanol/position_spec.rb +14 -0
  307. data/spec/parsanol/result_builder_spec.rb +391 -0
  308. data/spec/parsanol/rig/rspec_spec.rb +54 -0
  309. data/spec/parsanol/rope_spec.rb +207 -0
  310. data/spec/parsanol/scope_spec.rb +45 -0
  311. data/spec/parsanol/slice_spec.rb +249 -0
  312. data/spec/parsanol/source/line_cache_spec.rb +74 -0
  313. data/spec/parsanol/source_spec.rb +207 -0
  314. data/spec/parsanol/string_view_spec.rb +345 -0
  315. data/spec/parsanol/transform/context_spec.rb +56 -0
  316. data/spec/parsanol/transform_spec.rb +183 -0
  317. data/spec/parsanol/tree_memoization_spec.rb +149 -0
  318. data/spec/parslet_compatibility/expressir_edge_cases_spec.rb +153 -0
  319. data/spec/parslet_compatibility/minimal_reproduction.rb +199 -0
  320. data/spec/parslet_compatibility_spec.rb +399 -0
  321. data/spec/parslet_imported/atom_spec.rb +93 -0
  322. data/spec/parslet_imported/combinator_spec.rb +161 -0
  323. data/spec/parslet_imported/spec_helper.rb +73 -0
  324. data/spec/performance/batch_parsing_benchmark.rb +129 -0
  325. data/spec/performance/complete_optimization_summary.rb +143 -0
  326. data/spec/performance/grammar_caching_analysis.rb +121 -0
  327. data/spec/performance/grammar_caching_benchmark.rb +80 -0
  328. data/spec/performance/native_benchmark_spec.rb +230 -0
  329. data/spec/performance/phase5_benchmark.rb +144 -0
  330. data/spec/performance/profiling_benchmark.rb +131 -0
  331. data/spec/performance/ruby_improvements_benchmark.rb +171 -0
  332. data/spec/performance_spec.rb +374 -0
  333. data/spec/spec_helper.rb +79 -0
  334. data/spec/support/opal.rb +8 -0
  335. data/spec/support/opal.rb.erb +14 -0
  336. metadata +485 -0
@@ -0,0 +1,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Parsanol::BuilderCallbacks do
6
+ let(:builder_class) do
7
+ Class.new do
8
+ include Parsanol::BuilderCallbacks
9
+
10
+ attr_reader :events
11
+
12
+ def initialize
13
+ @events = []
14
+ end
15
+
16
+ def on_start(input)
17
+ @events << [:start, input]
18
+ end
19
+
20
+ def on_success
21
+ @events << [:success]
22
+ end
23
+
24
+ def on_error(message)
25
+ @events << [:error, message]
26
+ end
27
+
28
+ def on_string(value, offset, length)
29
+ @events << [:string, value, offset, length]
30
+ end
31
+
32
+ def on_int(value)
33
+ @events << [:int, value]
34
+ end
35
+
36
+ def on_float(value)
37
+ @events << [:float, value]
38
+ end
39
+
40
+ def on_bool(value)
41
+ @events << [:bool, value]
42
+ end
43
+
44
+ def on_nil
45
+ @events << [:nil]
46
+ end
47
+
48
+ def on_hash_start(size = nil)
49
+ @events << [:hash_start, size]
50
+ end
51
+
52
+ def on_hash_end(size)
53
+ @events << [:hash_end, size]
54
+ end
55
+
56
+ def on_hash_key(key)
57
+ @events << [:hash_key, key]
58
+ end
59
+
60
+ def on_hash_value(key)
61
+ @events << [:hash_value, key]
62
+ end
63
+
64
+ def on_array_start(size = nil)
65
+ @events << [:array_start, size]
66
+ end
67
+
68
+ def on_array_element(index)
69
+ @events << [:array_element, index]
70
+ end
71
+
72
+ def on_array_end(size)
73
+ @events << [:array_end, size]
74
+ end
75
+
76
+ def on_named_start(name)
77
+ @events << [:named_start, name]
78
+ end
79
+
80
+ def on_named_end(name)
81
+ @events << [:named_end, name]
82
+ end
83
+
84
+ def finish
85
+ @events
86
+ end
87
+ end
88
+ end
89
+
90
+ let(:builder) { builder_class.new }
91
+
92
+ describe 'included methods' do
93
+ it 'provides default no-op implementations' do
94
+ noop_builder = Class.new { include Parsanol::BuilderCallbacks }.new
95
+
96
+ expect { noop_builder.on_string('test', 0, 4) }.not_to raise_error
97
+ expect { noop_builder.on_int(42) }.not_to raise_error
98
+ expect { noop_builder.on_float(3.14) }.not_to raise_error
99
+ expect { noop_builder.on_bool(true) }.not_to raise_error
100
+ expect { noop_builder.on_nil }.not_to raise_error
101
+ expect { noop_builder.on_hash_start(nil) }.not_to raise_error
102
+ expect { noop_builder.on_hash_end(0) }.not_to raise_error
103
+ expect { noop_builder.on_hash_key('name') }.not_to raise_error
104
+ expect { noop_builder.on_array_start(nil) }.not_to raise_error
105
+ expect { noop_builder.on_array_element(0) }.not_to raise_error
106
+ expect { noop_builder.on_array_end(0) }.not_to raise_error
107
+ expect { noop_builder.on_named_start('test') }.not_to raise_error
108
+ expect { noop_builder.on_named_end('test') }.not_to raise_error
109
+ expect { noop_builder.on_start('input') }.not_to raise_error
110
+ expect { noop_builder.on_success }.not_to raise_error
111
+ expect { noop_builder.on_error('msg') }.not_to raise_error
112
+ expect { noop_builder.finish }.not_to raise_error
113
+ end
114
+ end
115
+
116
+ describe 'callback invocations' do
117
+ it 'records string callbacks' do
118
+ builder.on_string('hello', 0, 5)
119
+ expect(builder.events).to eq([[:string, 'hello', 0, 5]])
120
+ end
121
+
122
+ it 'records int callbacks' do
123
+ builder.on_int(42)
124
+ expect(builder.events).to eq([[:int, 42]])
125
+ end
126
+
127
+ it 'records float callbacks' do
128
+ builder.on_float(3.14)
129
+ expect(builder.events).to eq([[:float, 3.14]])
130
+ end
131
+
132
+ it 'records bool callbacks' do
133
+ builder.on_bool(true)
134
+ expect(builder.events).to eq([[:bool, true]])
135
+ end
136
+
137
+ it 'records nil callbacks' do
138
+ builder.on_nil
139
+ expect(builder.events).to eq([[:nil]])
140
+ end
141
+
142
+ it 'records hash callbacks' do
143
+ builder.on_hash_start(nil)
144
+ builder.on_hash_key('name')
145
+ builder.on_string('John', 0, 4)
146
+ builder.on_hash_end(1)
147
+
148
+ expect(builder.events).to eq([
149
+ [:hash_start, nil],
150
+ [:hash_key, 'name'],
151
+ [:string, 'John', 0, 4],
152
+ [:hash_end, 1]
153
+ ])
154
+ end
155
+
156
+ it 'records array callbacks' do
157
+ builder.on_array_start(nil)
158
+ builder.on_array_element(0)
159
+ builder.on_int(1)
160
+ builder.on_array_element(1)
161
+ builder.on_int(2)
162
+ builder.on_array_element(2)
163
+ builder.on_int(3)
164
+ builder.on_array_end(3)
165
+
166
+ expect(builder.events).to eq([
167
+ [:array_start, nil],
168
+ [:array_element, 0],
169
+ [:int, 1],
170
+ [:array_element, 1],
171
+ [:int, 2],
172
+ [:array_element, 2],
173
+ [:int, 3],
174
+ [:array_end, 3]
175
+ ])
176
+ end
177
+
178
+ it 'returns events from finish' do
179
+ builder.on_string('test', 0, 4)
180
+ expect(builder.finish).to eq([[:string, 'test', 0, 4]])
181
+ end
182
+ end
183
+ end
184
+
185
+ RSpec.describe Parsanol::Builders::DebugBuilder do
186
+ let(:builder) { Parsanol::Builders::DebugBuilder.new }
187
+
188
+ describe '#events' do
189
+ it 'collects all event types' do
190
+ builder.on_string('hello', 0, 5)
191
+ builder.on_int(42)
192
+ builder.on_float(3.14)
193
+ builder.on_bool(true)
194
+ builder.on_nil
195
+ builder.on_hash_start(nil)
196
+ builder.on_hash_key('name')
197
+ builder.on_hash_end(1)
198
+ builder.on_array_start(nil)
199
+ builder.on_array_end(0)
200
+
201
+ result = builder.finish
202
+
203
+ expect(result).to include('string: "hello" @ 0(5)')
204
+ expect(result).to include('int: 42')
205
+ expect(result).to include('float: 3.14')
206
+ expect(result).to include('bool: true')
207
+ expect(result).to include('nil')
208
+ expect(result).to include('hash_start(nil)')
209
+ expect(result).to include('hash_end(1)')
210
+ expect(result).to include('hash_key: "name"')
211
+ expect(result).to include('array_start(nil)')
212
+ expect(result).to include('array_end(0)')
213
+ end
214
+ end
215
+ end
216
+
217
+ RSpec.describe Parsanol::Builders::StringCollector do
218
+ let(:builder) { Parsanol::Builders::StringCollector.new }
219
+
220
+ describe '#strings' do
221
+ it 'collects only string values' do
222
+ builder.on_string('hello', 0, 5)
223
+ builder.on_int(42)
224
+ builder.on_string('world', 6, 5)
225
+
226
+ expect(builder.strings).to eq(['hello', 'world'])
227
+ end
228
+
229
+ it 'returns strings from finish' do
230
+ builder.on_string('test', 0, 4)
231
+ expect(builder.finish).to eq(['test'])
232
+ end
233
+ end
234
+ end
235
+
236
+ RSpec.describe Parsanol::Builders::NodeCounter do
237
+ let(:builder) { Parsanol::Builders::NodeCounter.new }
238
+
239
+ describe '#counts' do
240
+ it 'counts nodes by type' do
241
+ builder.on_string('a', 0, 1)
242
+ builder.on_string('b', 1, 1)
243
+ builder.on_int(1)
244
+ builder.on_int(2)
245
+ builder.on_int(3)
246
+ builder.on_float(1.0)
247
+ builder.on_bool(true)
248
+ builder.on_nil
249
+ builder.on_hash_start(nil)
250
+ builder.on_array_start(nil)
251
+ builder.on_array_start(nil)
252
+
253
+ expect(builder.counts[:string]).to eq(2)
254
+ expect(builder.counts[:int]).to eq(3)
255
+ expect(builder.counts[:float]).to eq(1)
256
+ expect(builder.counts[:bool]).to eq(1)
257
+ expect(builder.counts[:nil]).to eq(1)
258
+ expect(builder.counts[:hash]).to eq(1)
259
+ expect(builder.counts[:array]).to eq(2)
260
+ end
261
+
262
+ it 'returns counts from finish' do
263
+ builder.on_string('test', 0, 4)
264
+ builder.on_int(42)
265
+
266
+ result = builder.finish
267
+ expect(result[:string]).to eq(1)
268
+ expect(result[:int]).to eq(1)
269
+ end
270
+ end
271
+ end
272
+
273
+ RSpec.describe Parsanol::Parallel do
274
+ describe Parsanol::Parallel::Config do
275
+ let(:config) { Parsanol::Parallel::Config.new }
276
+
277
+ it 'has default values' do
278
+ expect(config.num_threads).to be_nil
279
+ expect(config.min_chunk_size).to eq(10)
280
+ end
281
+
282
+ it 'supports chaining' do
283
+ result = config.with_num_threads(8).with_min_chunk_size(50)
284
+
285
+ expect(result.num_threads).to eq(8)
286
+ expect(result.min_chunk_size).to eq(50)
287
+ expect(result).to eq(config)
288
+ end
289
+ end
290
+
291
+ describe '.available_cores' do
292
+ it 'returns an integer' do
293
+ result = Parsanol::Parallel.available_cores
294
+ expect(result).to be_a(Integer)
295
+ expect(result).to be >= 1
296
+ end
297
+ end
298
+
299
+ describe '.optimal_threads' do
300
+ it 'returns minimum of cores and input count' do
301
+ cores = Parsanol::Parallel.available_cores
302
+
303
+ # Small input count
304
+ expect(Parsanol::Parallel.optimal_threads(2)).to eq([2, cores].min)
305
+
306
+ # Large input count
307
+ expect(Parsanol::Parallel.optimal_threads(1000)).to eq(cores)
308
+ end
309
+ end
310
+
311
+ describe '.parse_batch' do
312
+ context 'when native extension is not available' do
313
+ before do
314
+ allow(Parsanol::Native).to receive(:available?).and_return(false)
315
+ end
316
+
317
+ it 'raises LoadError' do
318
+ expect {
319
+ Parsanol::Parallel.parse_batch('{}', ['input'])
320
+ }.to raise_error(LoadError, /requires native extension/)
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ RSpec.describe "Native parse_with_builder integration", :native do
327
+ # Define a simple parser for testing - captures only words, not spaces
328
+ let(:simple_parser) do
329
+ Class.new(Parsanol::Parser) do
330
+ rule(:word) { match('[a-z]+').as(:word) }
331
+ rule(:words) { word >> (match('\s') >> word).repeat }
332
+ root(:words)
333
+ end
334
+ end
335
+
336
+ describe "Parsanol::Native.parse_with_builder" do
337
+ context "when native extension is available" do
338
+ before do
339
+ skip "Native extension not available" unless Parsanol::Native.available?
340
+ end
341
+
342
+ it "parses input using a custom builder callback" do
343
+ grammar = Parsanol::Native.serialize_grammar(simple_parser.new.root)
344
+ input = "hello world"
345
+
346
+ builder = Parsanol::Builders::StringCollector.new
347
+ result = Parsanol::Native.parse_with_builder(grammar, input, builder)
348
+
349
+ # The parser captures all matches including spaces
350
+ expect(result).to include("hello", "world")
351
+ end
352
+
353
+ it "works with DebugBuilder" do
354
+ grammar = Parsanol::Native.serialize_grammar(simple_parser.new.root)
355
+ input = "a b"
356
+
357
+ builder = Parsanol::Builders::DebugBuilder.new
358
+ result = Parsanol::Native.parse_with_builder(grammar, input, builder)
359
+
360
+ expect(result).to be_a(String)
361
+ expect(result).to include("string:")
362
+ end
363
+
364
+ it "works with NodeCounter" do
365
+ grammar = Parsanol::Native.serialize_grammar(simple_parser.new.root)
366
+ input = "one two three"
367
+
368
+ builder = Parsanol::Builders::NodeCounter.new
369
+ result = Parsanol::Native.parse_with_builder(grammar, input, builder)
370
+
371
+ expect(result).to be_a(Hash)
372
+ # At minimum we have 3 words, plus spaces (5 total matches)
373
+ expect(result[:string]).to be >= 3
374
+ end
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Parsanol::Optimizer, '.simplify_choices' do
6
+ let(:str_a) { Parsanol::Atoms::Str.new('a') }
7
+ let(:str_b) { Parsanol::Atoms::Str.new('b') }
8
+ let(:str_c) { Parsanol::Atoms::Str.new('c') }
9
+
10
+ describe 'unwrapping single alternatives' do
11
+ it 'unwraps Alternative with single element' do
12
+ alt = Parsanol::Atoms::Alternative.new(str_a)
13
+ result = Parsanol::Optimizer.simplify_choices(alt)
14
+
15
+ expect(result).to eq(str_a)
16
+ end
17
+
18
+ it 'leaves Alternative with multiple elements unchanged' do
19
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_b)
20
+ result = Parsanol::Optimizer.simplify_choices(alt)
21
+
22
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
23
+ expect(result.alternatives.size).to eq(2)
24
+ end
25
+ end
26
+
27
+ describe 'flattening nested alternatives' do
28
+ it 'flattens Alternative(Alternative(a, b), c)' do
29
+ inner = Parsanol::Atoms::Alternative.new(str_a, str_b)
30
+ outer = Parsanol::Atoms::Alternative.new(inner, str_c)
31
+
32
+ result = Parsanol::Optimizer.simplify_choices(outer)
33
+
34
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
35
+ expect(result.alternatives.size).to eq(3)
36
+ expect(result.alternatives).to eq([str_a, str_b, str_c])
37
+ end
38
+
39
+ it 'flattens Alternative(a, Alternative(b, c))' do
40
+ inner = Parsanol::Atoms::Alternative.new(str_b, str_c)
41
+ outer = Parsanol::Atoms::Alternative.new(str_a, inner)
42
+
43
+ result = Parsanol::Optimizer.simplify_choices(outer)
44
+
45
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
46
+ expect(result.alternatives.size).to eq(3)
47
+ expect(result.alternatives).to eq([str_a, str_b, str_c])
48
+ end
49
+
50
+ it 'flattens Alternative(Alternative(a, b), Alternative(c, d))' do
51
+ inner1 = Parsanol::Atoms::Alternative.new(str_a, str_b)
52
+ inner2 = Parsanol::Atoms::Alternative.new(str_c, Parsanol::Atoms::Str.new('d'))
53
+ outer = Parsanol::Atoms::Alternative.new(inner1, inner2)
54
+
55
+ result = Parsanol::Optimizer.simplify_choices(outer)
56
+
57
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
58
+ expect(result.alternatives.size).to eq(4)
59
+ end
60
+
61
+ it 'flattens deeply nested alternatives' do
62
+ # Alternative(Alternative(Alternative(a, b), c), d)
63
+ inner1 = Parsanol::Atoms::Alternative.new(str_a, str_b)
64
+ inner2 = Parsanol::Atoms::Alternative.new(inner1, str_c)
65
+ outer = Parsanol::Atoms::Alternative.new(inner2, Parsanol::Atoms::Str.new('d'))
66
+
67
+ result = Parsanol::Optimizer.simplify_choices(outer)
68
+
69
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
70
+ expect(result.alternatives.size).to eq(4)
71
+ end
72
+ end
73
+
74
+ describe 'deduplicating alternatives' do
75
+ it 'removes duplicate alternatives' do
76
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_a, str_b)
77
+ result = Parsanol::Optimizer.simplify_choices(alt)
78
+
79
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
80
+ expect(result.alternatives.size).to eq(2)
81
+ expect(result.alternatives).to eq([str_a, str_b])
82
+ end
83
+
84
+ it 'removes all duplicates keeping first occurrence' do
85
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_b, str_a, str_c, str_b, str_a)
86
+ result = Parsanol::Optimizer.simplify_choices(alt)
87
+
88
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
89
+ expect(result.alternatives.size).to eq(3)
90
+ expect(result.alternatives).to eq([str_a, str_b, str_c])
91
+ end
92
+
93
+ it 'unwraps when deduplication leaves single alternative' do
94
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_a, str_a)
95
+ result = Parsanol::Optimizer.simplify_choices(alt)
96
+
97
+ expect(result).to eq(str_a)
98
+ end
99
+ end
100
+
101
+ describe 'combined optimizations' do
102
+ it 'flattens and deduplicates in one pass' do
103
+ # Alternative(Alternative(a, b), a, b)
104
+ inner = Parsanol::Atoms::Alternative.new(str_a, str_b)
105
+ outer = Parsanol::Atoms::Alternative.new(inner, str_a, str_b)
106
+
107
+ result = Parsanol::Optimizer.simplify_choices(outer)
108
+
109
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
110
+ expect(result.alternatives.size).to eq(2)
111
+ expect(result.alternatives).to eq([str_a, str_b])
112
+ end
113
+
114
+ it 'flattens, deduplicates, and unwraps' do
115
+ # Alternative(Alternative(a, a), a) => a
116
+ inner = Parsanol::Atoms::Alternative.new(str_a, str_a)
117
+ outer = Parsanol::Atoms::Alternative.new(inner, str_a)
118
+
119
+ result = Parsanol::Optimizer.simplify_choices(outer)
120
+
121
+ expect(result).to eq(str_a)
122
+ end
123
+ end
124
+
125
+ describe 'recursive optimization' do
126
+ it 'optimizes alternatives nested in sequences' do
127
+ # Sequence(Alternative(a, a), b) => Sequence(a, b)
128
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_a)
129
+ seq = Parsanol::Atoms::Sequence.new(alt, str_b)
130
+
131
+ result = Parsanol::Optimizer.simplify_choices(seq)
132
+
133
+ expect(result).to be_a(Parsanol::Atoms::Sequence)
134
+ expect(result.parslets[0]).to eq(str_a)
135
+ expect(result.parslets[1]).to eq(str_b)
136
+ end
137
+
138
+ it 'optimizes alternatives nested in repetitions' do
139
+ # Repetition(Alternative(a, a)) => Repetition(a)
140
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_a)
141
+ rep = Parsanol::Atoms::Repetition.new(alt, 0, nil, nil)
142
+
143
+ result = Parsanol::Optimizer.simplify_choices(rep)
144
+
145
+ expect(result).to be_a(Parsanol::Atoms::Repetition)
146
+ expect(result.parslet).to eq(str_a)
147
+ end
148
+
149
+ it 'optimizes alternatives nested in lookaheads' do
150
+ # Lookahead(Alternative(a, a)) => Lookahead(a)
151
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_a)
152
+ la = Parsanol::Atoms::Lookahead.new(alt, true)
153
+
154
+ result = Parsanol::Optimizer.simplify_choices(la)
155
+
156
+ expect(result).to be_a(Parsanol::Atoms::Lookahead)
157
+ expect(result.bound_parslet).to eq(str_a)
158
+ end
159
+
160
+ it 'optimizes alternatives nested in named atoms' do
161
+ # Named(Alternative(a, a)) => Named(a)
162
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_a)
163
+ named = Parsanol::Atoms::Named.new(alt, :test)
164
+
165
+ result = Parsanol::Optimizer.simplify_choices(named)
166
+
167
+ expect(result).to be_a(Parsanol::Atoms::Named)
168
+ expect(result.parslet).to eq(str_a)
169
+ end
170
+ end
171
+
172
+ describe 'preserving semantics' do
173
+ it 'does not modify leaf atoms' do
174
+ result = Parsanol::Optimizer.simplify_choices(str_a)
175
+ expect(result).to eq(str_a)
176
+ end
177
+
178
+ it 'preserves alternative order' do
179
+ alt = Parsanol::Atoms::Alternative.new(str_a, str_b, str_c)
180
+ result = Parsanol::Optimizer.simplify_choices(alt)
181
+
182
+ expect(result.alternatives).to eq([str_a, str_b, str_c])
183
+ end
184
+
185
+ it 'works with empty alternatives array (edge case)' do
186
+ # This shouldn't happen in practice, but test robustness
187
+ alt = Parsanol::Atoms::Alternative.new
188
+ result = Parsanol::Optimizer.simplify_choices(alt)
189
+
190
+ # Should return something reasonable (empty alternative or nil)
191
+ # The exact behavior depends on how Alternative handles empty case
192
+ end
193
+ end
194
+
195
+ describe 'functional test with parser' do
196
+ it 'correctly parses after optimization' do
197
+ # Create an alternative with duplicate choices
198
+ alt = str_a | str_b | str_a | str_c | str_b
199
+
200
+ # Optimize it
201
+ optimized = Parsanol::Optimizer.simplify_choices(alt)
202
+
203
+ # Should have 3 unique alternatives instead of 5
204
+ expect(optimized).to be_a(Parsanol::Atoms::Alternative)
205
+ expect(optimized.alternatives.size).to eq(3)
206
+
207
+ # Should still parse correctly
208
+ expect(optimized.parse('a')).to eq('a')
209
+ expect(optimized.parse('b')).to eq('b')
210
+ expect(optimized.parse('c')).to eq('c')
211
+ end
212
+
213
+ it 'correctly parses nested alternatives after optimization' do
214
+ # Create nested alternatives
215
+ alt = (str_a | str_b) | (str_c | Parsanol::Atoms::Str.new('d'))
216
+
217
+ # Optimize it
218
+ optimized = Parsanol::Optimizer.simplify_choices(alt)
219
+
220
+ # Should be flattened to 4 alternatives
221
+ expect(optimized).to be_a(Parsanol::Atoms::Alternative)
222
+ expect(optimized.alternatives.size).to eq(4)
223
+
224
+ # Should still parse all options
225
+ expect(optimized.parse('a')).to eq('a')
226
+ expect(optimized.parse('b')).to eq('b')
227
+ expect(optimized.parse('c')).to eq('c')
228
+ expect(optimized.parse('d')).to eq('d')
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'parslet/convenience' do
4
+ require 'parsanol/convenience'
5
+ include Parsanol
6
+
7
+ class FooParser < Parsanol::Parser
8
+ rule(:foo) { str('foo') }
9
+ root(:foo)
10
+ end
11
+
12
+ describe 'parse_with_debug' do
13
+ let(:parser) { FooParser.new }
14
+
15
+ context 'internal' do
16
+ before do
17
+ # Suppress output.
18
+ #
19
+ allow(parser).to receive(:puts)
20
+ end
21
+
22
+ it 'exists' do
23
+ -> { parser.parse_with_debug('anything') }.should_not raise_error
24
+ end
25
+
26
+ it 'catches ParseFailed exceptions' do
27
+ -> { parser.parse_with_debug('bar') }.should_not raise_error
28
+ end
29
+
30
+ it 'parses correct input like #parse' do
31
+ -> { parser.parse_with_debug('foo') }.should_not raise_error
32
+ end
33
+ end
34
+
35
+ context 'output' do
36
+ it 'putses once for tree output' do
37
+ expect(parser).to receive(:puts).once
38
+
39
+ parser.parse_with_debug('incorrect')
40
+ end
41
+
42
+ it 'putses once for the error on unconsumed input' do
43
+ expect(parser).to receive(:puts).once
44
+
45
+ parser.parse_with_debug('foobar')
46
+ end
47
+ end
48
+
49
+ it 'works for all parslets' do
50
+ str('foo').parse_with_debug('foo')
51
+ match['bar'].parse_with_debug('a')
52
+ end
53
+ end
54
+ end