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,459 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Parsanol::Optimizer do
6
+ include Parsanol
7
+
8
+ describe '.simplify_quantifiers' do
9
+ context 'with trivial repetitions' do
10
+ it 'unwraps repeat(1, 1) to just the inner parslet' do
11
+ parser = str('a').repeat(1, 1)
12
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
13
+
14
+ expect(simplified).to be_a(Parsanol::Atoms::Str)
15
+ expect(simplified.str).to eq('a')
16
+ end
17
+
18
+ it 'preserves repeat(0, 1) (maybe)' do
19
+ parser = str('a').repeat(0, 1)
20
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
21
+
22
+ expect(simplified).to be_a(Parsanol::Atoms::Repetition)
23
+ expect(simplified.min).to eq(0)
24
+ expect(simplified.max).to eq(1)
25
+ end
26
+
27
+ it 'preserves repeat(0, nil) (zero or more)' do
28
+ parser = str('a').repeat(0)
29
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
30
+
31
+ expect(simplified).to be_a(Parsanol::Atoms::Repetition)
32
+ expect(simplified.min).to eq(0)
33
+ expect(simplified.max).to be_nil
34
+ end
35
+
36
+ it 'preserves repeat(1, nil) (one or more)' do
37
+ parser = str('a').repeat(1)
38
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
39
+
40
+ expect(simplified).to be_a(Parsanol::Atoms::Repetition)
41
+ expect(simplified.min).to eq(1)
42
+ expect(simplified.max).to be_nil
43
+ end
44
+ end
45
+
46
+ context 'with nested repetitions' do
47
+ it 'flattens repeat(0, 1).repeat(0, 1) to repeat(0, 1)' do
48
+ parser = str('a').repeat(0, 1).repeat(0, 1)
49
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
50
+
51
+ expect(simplified).to be_a(Parsanol::Atoms::Repetition)
52
+ expect(simplified.min).to eq(0)
53
+ expect(simplified.max).to eq(1)
54
+ expect(simplified.parslet).to be_a(Parsanol::Atoms::Str)
55
+ end
56
+
57
+ it 'multiplies exact counts: repeat(2, 2).repeat(3, 3) => repeat(6, 6)' do
58
+ parser = str('a').repeat(2, 2).repeat(3, 3)
59
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
60
+
61
+ expect(simplified).to be_a(Parsanol::Atoms::Repetition)
62
+ expect(simplified.min).to eq(6)
63
+ expect(simplified.max).to eq(6)
64
+ expect(simplified.parslet).to be_a(Parsanol::Atoms::Str)
65
+ end
66
+
67
+ it 'multiplies exact counts: repeat(4, 4).repeat(2, 2) => repeat(8, 8)' do
68
+ parser = str('x').repeat(4, 4).repeat(2, 2)
69
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
70
+
71
+ expect(simplified).to be_a(Parsanol::Atoms::Repetition)
72
+ expect(simplified.min).to eq(8)
73
+ expect(simplified.max).to eq(8)
74
+ end
75
+
76
+ it 'does not simplify variable repetitions' do
77
+ parser = str('a').repeat(1, 3).repeat(2, 5)
78
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
79
+
80
+ # Should still be nested but with simplified child
81
+ expect(simplified).to be_a(Parsanol::Atoms::Repetition)
82
+ expect(simplified.min).to eq(2)
83
+ expect(simplified.max).to eq(5)
84
+ expect(simplified.parslet).to be_a(Parsanol::Atoms::Repetition)
85
+ expect(simplified.parslet.min).to eq(1)
86
+ expect(simplified.parslet.max).to eq(3)
87
+ end
88
+ end
89
+
90
+ context 'with sequences' do
91
+ it 'simplifies repetitions within sequences' do
92
+ parser = str('a').repeat(1, 1) >> str('b') >> str('c').repeat(1, 1)
93
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
94
+
95
+ expect(simplified).to be_a(Parsanol::Atoms::Sequence)
96
+ expect(simplified.parslets.size).to eq(3)
97
+ expect(simplified.parslets[0]).to be_a(Parsanol::Atoms::Str)
98
+ expect(simplified.parslets[0].str).to eq('a')
99
+ expect(simplified.parslets[1]).to be_a(Parsanol::Atoms::Str)
100
+ expect(simplified.parslets[1].str).to eq('b')
101
+ expect(simplified.parslets[2]).to be_a(Parsanol::Atoms::Str)
102
+ expect(simplified.parslets[2].str).to eq('c')
103
+ end
104
+
105
+ it 'preserves sequences with non-trivial repetitions' do
106
+ parser = str('a').repeat(0, 1) >> str('b').repeat(1)
107
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
108
+
109
+ expect(simplified).to be_a(Parsanol::Atoms::Sequence)
110
+ expect(simplified.parslets.size).to eq(2)
111
+ expect(simplified.parslets[0]).to be_a(Parsanol::Atoms::Repetition)
112
+ expect(simplified.parslets[1]).to be_a(Parsanol::Atoms::Repetition)
113
+ end
114
+
115
+ it 'handles mixed simplifiable and non-simplifiable repetitions' do
116
+ parser = str('a').repeat(1, 1) >> str('b').repeat(0, 1) >> str('c')
117
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
118
+
119
+ expect(simplified).to be_a(Parsanol::Atoms::Sequence)
120
+ expect(simplified.parslets.size).to eq(3)
121
+ expect(simplified.parslets[0]).to be_a(Parsanol::Atoms::Str)
122
+ expect(simplified.parslets[1]).to be_a(Parsanol::Atoms::Repetition)
123
+ expect(simplified.parslets[2]).to be_a(Parsanol::Atoms::Str)
124
+ end
125
+ end
126
+
127
+ context 'with alternatives' do
128
+ it 'simplifies repetitions within alternatives' do
129
+ parser = str('a').repeat(1, 1) | str('b').repeat(1, 1)
130
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
131
+
132
+ expect(simplified).to be_a(Parsanol::Atoms::Alternative)
133
+ expect(simplified.alternatives.size).to eq(2)
134
+ expect(simplified.alternatives[0]).to be_a(Parsanol::Atoms::Str)
135
+ expect(simplified.alternatives[0].str).to eq('a')
136
+ expect(simplified.alternatives[1]).to be_a(Parsanol::Atoms::Str)
137
+ expect(simplified.alternatives[1].str).to eq('b')
138
+ end
139
+
140
+ it 'handles mixed cases in alternatives' do
141
+ parser = str('a').repeat(1, 1) | str('b').repeat(0, 1) | str('c')
142
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
143
+
144
+ expect(simplified).to be_a(Parsanol::Atoms::Alternative)
145
+ expect(simplified.alternatives.size).to eq(3)
146
+ expect(simplified.alternatives[0]).to be_a(Parsanol::Atoms::Str)
147
+ expect(simplified.alternatives[1]).to be_a(Parsanol::Atoms::Repetition)
148
+ expect(simplified.alternatives[2]).to be_a(Parsanol::Atoms::Str)
149
+ end
150
+ end
151
+
152
+ context 'with lookaheads' do
153
+ it 'simplifies repetitions within positive lookahead' do
154
+ parser = str('a').repeat(1, 1).present?
155
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
156
+
157
+ expect(simplified).to be_a(Parsanol::Atoms::Lookahead)
158
+ expect(simplified.bound_parslet).to be_a(Parsanol::Atoms::Str)
159
+ expect(simplified.positive).to be true
160
+ end
161
+
162
+ it 'simplifies repetitions within negative lookahead' do
163
+ parser = str('a').repeat(1, 1).absent?
164
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
165
+
166
+ expect(simplified).to be_a(Parsanol::Atoms::Lookahead)
167
+ expect(simplified.bound_parslet).to be_a(Parsanol::Atoms::Str)
168
+ expect(simplified.positive).to be false
169
+ end
170
+ end
171
+
172
+ context 'with named parslets' do
173
+ it 'simplifies repetitions within named parslets' do
174
+ parser = str('a').repeat(1, 1).as(:foo)
175
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
176
+
177
+ expect(simplified).to be_a(Parsanol::Atoms::Named)
178
+ expect(simplified.name).to eq(:foo)
179
+ expect(simplified.parslet).to be_a(Parsanol::Atoms::Str)
180
+ expect(simplified.parslet.str).to eq('a')
181
+ end
182
+ end
183
+
184
+ context 'with complex nested structures' do
185
+ it 'simplifies deeply nested repetitions' do
186
+ # ((a.repeat(1,1)).repeat(1,1)).repeat(1,1)
187
+ parser = str('a').repeat(1, 1).repeat(1, 1).repeat(1, 1)
188
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
189
+
190
+ expect(simplified).to be_a(Parsanol::Atoms::Str)
191
+ expect(simplified.str).to eq('a')
192
+ end
193
+
194
+ it 'simplifies complex sequences with multiple repetitions' do
195
+ # (a.repeat(1,1) >> b.repeat(2,2).repeat(3,3) >> c.repeat(0,1))
196
+ parser = str('a').repeat(1, 1) >>
197
+ str('b').repeat(2, 2).repeat(3, 3) >>
198
+ str('c').repeat(0, 1)
199
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
200
+
201
+ expect(simplified).to be_a(Parsanol::Atoms::Sequence)
202
+ expect(simplified.parslets.size).to eq(3)
203
+
204
+ # First element: unwrapped 'a'
205
+ expect(simplified.parslets[0]).to be_a(Parsanol::Atoms::Str)
206
+ expect(simplified.parslets[0].str).to eq('a')
207
+
208
+ # Second element: flattened to repeat(6, 6)
209
+ expect(simplified.parslets[1]).to be_a(Parsanol::Atoms::Repetition)
210
+ expect(simplified.parslets[1].min).to eq(6)
211
+ expect(simplified.parslets[1].max).to eq(6)
212
+ expect(simplified.parslets[1].parslet).to be_a(Parsanol::Atoms::Str)
213
+
214
+ # Third element: preserved maybe
215
+ expect(simplified.parslets[2]).to be_a(Parsanol::Atoms::Repetition)
216
+ expect(simplified.parslets[2].min).to eq(0)
217
+ expect(simplified.parslets[2].max).to eq(1)
218
+ end
219
+
220
+ it 'handles sequences within alternatives with repetitions' do
221
+ parser = (str('a').repeat(1, 1) >> str('b')) |
222
+ (str('c').repeat(1, 1) >> str('d'))
223
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
224
+
225
+ expect(simplified).to be_a(Parsanol::Atoms::Alternative)
226
+ expect(simplified.alternatives.size).to eq(2)
227
+
228
+ # First alternative: sequence with 'a' and 'b'
229
+ alt1 = simplified.alternatives[0]
230
+ expect(alt1).to be_a(Parsanol::Atoms::Sequence)
231
+ expect(alt1.parslets[0]).to be_a(Parsanol::Atoms::Str)
232
+ expect(alt1.parslets[0].str).to eq('a')
233
+
234
+ # Second alternative: sequence with 'c' and 'd'
235
+ alt2 = simplified.alternatives[1]
236
+ expect(alt2).to be_a(Parsanol::Atoms::Sequence)
237
+ expect(alt2.parslets[0]).to be_a(Parsanol::Atoms::Str)
238
+ expect(alt2.parslets[0].str).to eq('c')
239
+ end
240
+ end
241
+
242
+ context 'with leaf nodes' do
243
+ it 'leaves Str atoms unchanged' do
244
+ parser = str('hello')
245
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
246
+
247
+ expect(simplified).to equal(parser)
248
+ end
249
+
250
+ it 'leaves Re atoms unchanged' do
251
+ parser = match['a-z']
252
+ simplified = Parsanol::Optimizer.simplify_quantifiers(parser)
253
+
254
+ expect(simplified).to equal(parser)
255
+ end
256
+ end
257
+
258
+ context 'semantic preservation' do
259
+ it 'produces equivalent parse results for unwrapped repeat(1,1)' do
260
+ original = str('test').repeat(1, 1)
261
+ simplified = Parsanol::Optimizer.simplify_quantifiers(original)
262
+
263
+ input = 'test'
264
+ expect(original.parse(input)).to eq(simplified.parse(input))
265
+ end
266
+
267
+ it 'produces equivalent parse results for flattened maybe.maybe' do
268
+ original = str('x').repeat(0, 1).repeat(0, 1)
269
+ simplified = Parsanol::Optimizer.simplify_quantifiers(original)
270
+
271
+ # Test with match
272
+ expect(original.parse('x')).to eq(simplified.parse('x'))
273
+ # Test without match
274
+ expect(original.parse('')).to eq(simplified.parse(''))
275
+ end
276
+
277
+ it 'produces equivalent parse results for multiplied exact counts' do
278
+ original = str('a').repeat(2, 2).repeat(3, 3)
279
+ simplified = Parsanol::Optimizer.simplify_quantifiers(original)
280
+
281
+ input = 'aaaaaa'
282
+ expect(original.parse(input)).to eq(simplified.parse(input))
283
+ end
284
+
285
+ it 'produces equivalent parse results for complex sequences' do
286
+ original = str('a').repeat(1, 1) >> str('b') >> str('c').repeat(0, 1)
287
+ simplified = Parsanol::Optimizer.simplify_quantifiers(original)
288
+
289
+ # Test with optional 'c'
290
+ expect(original.parse('abc')).to eq(simplified.parse('abc'))
291
+ # Test without optional 'c'
292
+ expect(original.parse('ab')).to eq(simplified.parse('ab'))
293
+ end
294
+ end
295
+ end
296
+
297
+ describe '.simplify_sequences' do
298
+ context 'string merging' do
299
+ it 'merges adjacent str atoms' do
300
+ # str('a') >> str('b') >> str('c') => str('abc')
301
+ sequence = str('a') >> str('b') >> str('c')
302
+ result = Parsanol::Optimizer.simplify_sequences(sequence)
303
+
304
+ expect(result).to be_a(Parsanol::Atoms::Str)
305
+ expect(result.str).to eq('abc')
306
+ end
307
+
308
+ it 'merges only adjacent strings' do
309
+ # str('a') >> str('b') >> match['x'] >> str('c') >> str('d')
310
+ # => str('ab') >> match['x'] >> str('cd')
311
+ sequence = str('a') >> str('b') >> match['x'] >> str('c') >> str('d')
312
+ result = Parsanol::Optimizer.simplify_sequences(sequence)
313
+
314
+ expect(result).to be_a(Parsanol::Atoms::Sequence)
315
+ expect(result.parslets.size).to eq(3)
316
+ expect(result.parslets[0].str).to eq('ab')
317
+ expect(result.parslets[1]).to be_a(Parsanol::Atoms::Re)
318
+ expect(result.parslets[2].str).to eq('cd')
319
+ end
320
+
321
+ it 'handles empty strings' do
322
+ sequence = str('a') >> str('') >> str('b')
323
+ result = Parsanol::Optimizer.simplify_sequences(sequence)
324
+
325
+ expect(result).to be_a(Parsanol::Atoms::Str)
326
+ expect(result.str).to eq('ab')
327
+ end
328
+ end
329
+
330
+ context 'sequence flattening' do
331
+ it 'flattens nested sequences' do
332
+ # (str('a') >> str('b')) >> (str('c') >> str('d'))
333
+ # => str('abcd')
334
+ inner1 = str('a') >> str('b')
335
+ inner2 = str('c') >> str('d')
336
+ nested = inner1 >> inner2
337
+ result = Parsanol::Optimizer.simplify_sequences(nested)
338
+
339
+ expect(result).to be_a(Parsanol::Atoms::Str)
340
+ expect(result.str).to eq('abcd')
341
+ end
342
+
343
+ it 'flattens deeply nested sequences' do
344
+ # ((str('a') >> str('b')) >> str('c')) >> str('d')
345
+ # => str('abcd')
346
+ nested = ((str('a') >> str('b')) >> str('c')) >> str('d')
347
+ result = Parsanol::Optimizer.simplify_sequences(nested)
348
+
349
+ expect(result).to be_a(Parsanol::Atoms::Str)
350
+ expect(result.str).to eq('abcd')
351
+ end
352
+ end
353
+
354
+ context 'sequence unwrapping' do
355
+ it 'unwraps single-element sequences' do
356
+ # str('a') >> (nothing else) => str('a')
357
+ sequence = Parsanol::Atoms::Sequence.new(str('a'))
358
+ result = Parsanol::Optimizer.simplify_sequences(sequence)
359
+
360
+ expect(result).to be_a(Parsanol::Atoms::Str)
361
+ expect(result.str).to eq('a')
362
+ end
363
+ end
364
+
365
+ context 'recursive simplification' do
366
+ it 'simplifies sequences in alternatives' do
367
+ # (str('a') >> str('b')) | (str('c') >> str('d'))
368
+ # => str('ab') | str('cd')
369
+ alt = (str('a') >> str('b')) | (str('c') >> str('d'))
370
+ result = Parsanol::Optimizer.simplify_sequences(alt)
371
+
372
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
373
+ expect(result.alternatives[0]).to be_a(Parsanol::Atoms::Str)
374
+ expect(result.alternatives[0].str).to eq('ab')
375
+ expect(result.alternatives[1]).to be_a(Parsanol::Atoms::Str)
376
+ expect(result.alternatives[1].str).to eq('cd')
377
+ end
378
+
379
+ it 'simplifies sequences in repetitions' do
380
+ # (str('a') >> str('b')).repeat(2, 2)
381
+ # => str('ab').repeat(2, 2)
382
+ rep = (str('a') >> str('b')).repeat(2, 2)
383
+ result = Parsanol::Optimizer.simplify_sequences(rep)
384
+
385
+ expect(result).to be_a(Parsanol::Atoms::Repetition)
386
+ expect(result.parslet).to be_a(Parsanol::Atoms::Str)
387
+ expect(result.parslet.str).to eq('ab')
388
+ end
389
+
390
+ it 'simplifies sequences in lookaheads' do
391
+ # (str('a') >> str('b')).present?
392
+ # => str('ab').present?
393
+ la = (str('a') >> str('b')).present?
394
+ result = Parsanol::Optimizer.simplify_sequences(la)
395
+
396
+ expect(result).to be_a(Parsanol::Atoms::Lookahead)
397
+ expect(result.bound_parslet).to be_a(Parsanol::Atoms::Str)
398
+ expect(result.bound_parslet.str).to eq('ab')
399
+ end
400
+
401
+ it 'simplifies sequences in named parslets' do
402
+ # (str('a') >> str('b')).as(:test)
403
+ # => str('ab').as(:test)
404
+ named = (str('a') >> str('b')).as(:test)
405
+ result = Parsanol::Optimizer.simplify_sequences(named)
406
+
407
+ expect(result).to be_a(Parsanol::Atoms::Named)
408
+ expect(result.parslet).to be_a(Parsanol::Atoms::Str)
409
+ expect(result.parslet.str).to eq('ab')
410
+ end
411
+ end
412
+
413
+ context 'semantic preservation' do
414
+ it 'produces same parse results after optimization' do
415
+ # Test that optimization doesn't change semantics
416
+ original = str('h') >> str('e') >> str('l') >> str('l') >> str('o')
417
+ optimized = Parsanol::Optimizer.simplify_sequences(original)
418
+
419
+ input = 'hello'
420
+ expect(original.parse(input)).to eq(optimized.parse(input))
421
+ end
422
+
423
+ it 'preserves parsing with non-string elements' do
424
+ original = str('a') >> match['0-9'] >> str('b')
425
+ optimized = Parsanol::Optimizer.simplify_sequences(original)
426
+
427
+ input = 'a5b'
428
+ expect(original.parse(input)).to eq(optimized.parse(input))
429
+ end
430
+ end
431
+
432
+ context 'edge cases' do
433
+ it 'handles sequences with only non-string elements' do
434
+ sequence = match['a'] >> match['b'] >> match['c']
435
+ result = Parsanol::Optimizer.simplify_sequences(sequence)
436
+
437
+ # Should remain unchanged
438
+ expect(result).to be_a(Parsanol::Atoms::Sequence)
439
+ expect(result.parslets.size).to eq(3)
440
+ end
441
+
442
+ it 'handles empty sequences' do
443
+ sequence = Parsanol::Atoms::Sequence.new()
444
+ result = Parsanol::Optimizer.simplify_sequences(sequence)
445
+
446
+ # Empty sequence stays as sequence (or could unwrap to nil)
447
+ expect(result).to be_a(Parsanol::Atoms::Sequence)
448
+ end
449
+
450
+ it 'handles single string in sequence' do
451
+ sequence = Parsanol::Atoms::Sequence.new(str('test'))
452
+ result = Parsanol::Optimizer.simplify_sequences(sequence)
453
+
454
+ expect(result).to be_a(Parsanol::Atoms::Str)
455
+ expect(result.str).to eq('test')
456
+ end
457
+ end
458
+ end
459
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Parsanol::Parslet compatibility layer' do
6
+ describe 'module structure' do
7
+ it 'provides Parsanol::Parslet as a nested module' do
8
+ expect(defined?(Parsanol::Parslet)).to eq('constant')
9
+ expect(Parsanol::Parslet).to be_a(Module)
10
+ end
11
+
12
+ it 'provides Parslet-compatible API through nested module' do
13
+ expect(Parsanol::Parslet::Parser).to eq(Parsanol::Parser)
14
+ expect(Parsanol::Parslet::Transform).to eq(Parsanol::Transform)
15
+ end
16
+ end
17
+
18
+ describe 'DSL methods' do
19
+ it 'delegates match to Parsanol' do
20
+ expect(Parsanol::Parslet.match('[a-z]')).to be_a(Parsanol::Atoms::Re)
21
+ end
22
+
23
+ it 'delegates str to Parsanol' do
24
+ expect(Parsanol::Parslet.str('hello')).to be_a(Parsanol::Atoms::Str)
25
+ end
26
+
27
+ it 'delegates any to Parsanol' do
28
+ expect(Parsanol::Parslet.any).to be_a(Parsanol::Atoms::Re)
29
+ end
30
+
31
+ it 'delegates simple to Parsanol' do
32
+ expect(Parsanol::Parslet.simple(:x)).to be_a(Parsanol::Pattern::SimpleBind)
33
+ end
34
+
35
+ it 'delegates sequence to Parsanol' do
36
+ expect(Parsanol::Parslet.sequence(:x)).to be_a(Parsanol::Pattern::SequenceBind)
37
+ end
38
+
39
+ it 'delegates subtree to Parsanol' do
40
+ expect(Parsanol::Parslet.subtree(:x)).to be_a(Parsanol::Pattern::SubtreeBind)
41
+ end
42
+ end
43
+
44
+ describe 'class aliases' do
45
+ it 'aliases Parser to Parsanol::Parser' do
46
+ expect(Parsanol::Parslet::Parser).to eq(Parsanol::Parser)
47
+ end
48
+
49
+ it 'aliases Transform to Parsanol::Transform' do
50
+ expect(Parsanol::Parslet::Transform).to eq(Parsanol::Transform)
51
+ end
52
+
53
+ it 'aliases Slice to Parsanol::Slice' do
54
+ expect(Parsanol::Parslet::Slice).to eq(Parsanol::Slice)
55
+ end
56
+
57
+ it 'aliases Source to Parsanol::Source' do
58
+ expect(Parsanol::Parslet::Source).to eq(Parsanol::Source)
59
+ end
60
+
61
+ it 'aliases Cause to Parsanol::Cause' do
62
+ expect(Parsanol::Parslet::Cause).to eq(Parsanol::Cause)
63
+ end
64
+
65
+ it 'aliases Pattern to Parsanol::Pattern' do
66
+ expect(Parsanol::Parslet::Pattern).to eq(Parsanol::Pattern)
67
+ end
68
+ end
69
+
70
+ describe 'Atoms module' do
71
+ it 'aliases Base to Parsanol::Atoms::Base' do
72
+ expect(Parsanol::Parslet::Atoms::Base).to eq(Parsanol::Atoms::Base)
73
+ end
74
+
75
+ it 'aliases Str to Parsanol::Atoms::Str' do
76
+ expect(Parsanol::Parslet::Atoms::Str).to eq(Parsanol::Atoms::Str)
77
+ end
78
+
79
+ it 'aliases Re to Parsanol::Atoms::Re' do
80
+ expect(Parsanol::Parslet::Atoms::Re).to eq(Parsanol::Atoms::Re)
81
+ end
82
+
83
+ it 'aliases Sequence to Parsanol::Atoms::Sequence' do
84
+ expect(Parsanol::Parslet::Atoms::Sequence).to eq(Parsanol::Atoms::Sequence)
85
+ end
86
+
87
+ it 'aliases Alternative to Parsanol::Atoms::Alternative' do
88
+ expect(Parsanol::Parslet::Atoms::Alternative).to eq(Parsanol::Atoms::Alternative)
89
+ end
90
+
91
+ it 'aliases Repetition to Parsanol::Atoms::Repetition' do
92
+ expect(Parsanol::Parslet::Atoms::Repetition).to eq(Parsanol::Atoms::Repetition)
93
+ end
94
+
95
+ it 'aliases Named to Parsanol::Atoms::Named' do
96
+ expect(Parsanol::Parslet::Atoms::Named).to eq(Parsanol::Atoms::Named)
97
+ end
98
+
99
+ it 'aliases Entity to Parsanol::Atoms::Entity' do
100
+ expect(Parsanol::Parslet::Atoms::Entity).to eq(Parsanol::Atoms::Entity)
101
+ end
102
+
103
+ it 'aliases Lookahead to Parsanol::Atoms::Lookahead' do
104
+ expect(Parsanol::Parslet::Atoms::Lookahead).to eq(Parsanol::Atoms::Lookahead)
105
+ end
106
+
107
+ it 'aliases Cut to Parsanol::Atoms::Cut' do
108
+ expect(Parsanol::Parslet::Atoms::Cut).to eq(Parsanol::Atoms::Cut)
109
+ end
110
+
111
+ it 'aliases Capture to Parsanol::Atoms::Capture' do
112
+ expect(Parsanol::Parslet::Atoms::Capture).to eq(Parsanol::Atoms::Capture)
113
+ end
114
+
115
+ it 'aliases Scope to Parsanol::Atoms::Scope' do
116
+ expect(Parsanol::Parslet::Atoms::Scope).to eq(Parsanol::Atoms::Scope)
117
+ end
118
+
119
+ it 'aliases Dynamic to Parsanol::Atoms::Dynamic' do
120
+ expect(Parsanol::Parslet::Atoms::Dynamic).to eq(Parsanol::Atoms::Dynamic)
121
+ end
122
+
123
+ it 'aliases Infix to Parsanol::Atoms::Infix' do
124
+ expect(Parsanol::Parslet::Atoms::Infix).to eq(Parsanol::Atoms::Infix)
125
+ end
126
+
127
+ it 'aliases Ignored to Parsanol::Atoms::Ignored' do
128
+ expect(Parsanol::Parslet::Atoms::Ignored).to eq(Parsanol::Atoms::Ignored)
129
+ end
130
+
131
+ it 'aliases ParseFailed to Parsanol::ParseFailed' do
132
+ expect(Parsanol::Parslet::Atoms::ParseFailed).to eq(Parsanol::ParseFailed)
133
+ end
134
+ end
135
+
136
+ describe 'parser usage' do
137
+ let(:parser_class) do
138
+ Class.new(Parsanol::Parslet::Parser) do
139
+ include Parsanol::Parslet
140
+
141
+ rule(:number) { match('[0-9]').repeat(1).as(:int) }
142
+ root(:number)
143
+ end
144
+ end
145
+
146
+ it 'parses input correctly' do
147
+ parser = parser_class.new
148
+ result = parser.parse('42')
149
+ expect(result).to eq({ int: '42' })
150
+ end
151
+ end
152
+
153
+ describe 'transform usage' do
154
+ let(:transform_class) do
155
+ Class.new(Parsanol::Parslet::Transform) do
156
+ rule(int: simple(:n)) { Integer(n) }
157
+ end
158
+ end
159
+
160
+ it 'transforms input correctly' do
161
+ transform = transform_class.new
162
+ result = transform.apply({ int: '42' })
163
+ expect(result).to eq(42)
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Parsanol::RubyTransform do
6
+ let(:parser_class) do
7
+ Class.new(Parsanol::Parser) do
8
+ include Parsanol::RubyTransform
9
+
10
+ rule(:number) { match('[0-9]').repeat(1).as(:int) }
11
+ rule(:space?) { match('\s').repeat }
12
+ rule(:add_op) { space? >> match('[+-]').as(:op) >> space? }
13
+ rule(:expression) { (number.as(:left) >> add_op.as(:op) >> expression.as(:right)).as(:binop) | number }
14
+ root(:expression)
15
+ end
16
+ end
17
+
18
+ let(:parser) { parser_class.new }
19
+
20
+ describe '.parse_backend' do
21
+ it 'defaults to :ruby' do
22
+ expect(parser_class.parse_backend).to eq(:ruby)
23
+ end
24
+
25
+ it 'can be set to :rust' do
26
+ parser_class.parse_backend = :rust
27
+ expect(parser_class.parse_backend).to eq(:rust)
28
+ end
29
+ end
30
+
31
+ describe '.use_rust_backend!' do
32
+ context 'when native extension is not available' do
33
+ before do
34
+ allow(Parsanol::Native).to receive(:available?).and_return(false)
35
+ end
36
+
37
+ it 'raises LoadError' do
38
+ expect { parser_class.use_rust_backend! }.to raise_error(LoadError, /Rust backend requested/)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe '#parse' do
44
+ it 'parses input and returns a tree' do
45
+ result = parser.parse('42')
46
+ expect(result).to eq({ int: '42' })
47
+ end
48
+
49
+ it 'parses complex expressions' do
50
+ result = parser.parse('1 + 2')
51
+ expect(result).to be_a(Hash)
52
+ expect(result[:binop][:left]).to eq({ int: '1' })
53
+ end
54
+ end
55
+
56
+ describe '#parse_with_transform' do
57
+ let(:transform) do
58
+ Class.new(Parsanol::Transform) do
59
+ rule(int: simple(:n)) { Integer(n) }
60
+ rule(left: simple(:l), op: { op: simple(:o) }, right: simple(:r)) { { left: l, op: o, right: r } }
61
+ rule(binop: simple(:b)) { b }
62
+ end.new
63
+ end
64
+
65
+ it 'parses and transforms in one step' do
66
+ result = parser.parse_with_transform('42', transform)
67
+ expect(result).to eq(42)
68
+ end
69
+ end
70
+ end