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,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ begin
5
+ require 'benchmark/ips'
6
+ rescue LoadError
7
+ # Skip this spec file if benchmark/ips is not available
8
+ return unless defined?(RSpec)
9
+ RSpec.describe "Performance Regression Tests", :performance do
10
+ it "requires benchmark-ips gem for performance tests" do
11
+ skip "benchmark-ips gem not installed. Install with: gem install benchmark-ips"
12
+ end
13
+ end
14
+ return
15
+ end
16
+
17
+ RSpec.describe "Performance Regression Tests", :performance do
18
+ # Baseline performance expectations (adjusted for opt-in optimization model)
19
+ # These are conservative targets that should pass on most systems
20
+ # Note: These thresholds are set low to accommodate CI environments
21
+ # and debug builds. Production builds should be significantly faster.
22
+ BASELINE_IPS = {
23
+ simple_calc: 500, # Adjusted for CI/debug environments
24
+ json_parse: 400, # Adjusted for CI/debug environments
25
+ xml_parse: 400 # Adjusted for CI/debug environments
26
+ }.freeze
27
+
28
+ # Note: The 13.3x speedup is cumulative from all optimization phases (1-50b)
29
+ # vs parslet 2.0, not just from optimize_rules! alone
30
+
31
+ # Test parsers from examples
32
+ let(:calc_parser) do
33
+ Class.new(Parsanol::Parser) do
34
+ optimize_rules!
35
+
36
+ rule(:addition) {
37
+ multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
38
+ multiplication
39
+ }
40
+
41
+ rule(:multiplication) {
42
+ integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
43
+ integer
44
+ }
45
+
46
+ rule(:integer) { digit.repeat(1).as(:i) >> space? }
47
+ rule(:mult_op) { match['*/'].as(:o) >> space? }
48
+ rule(:add_op) { match['+-'].as(:o) >> space? }
49
+ rule(:digit) { match['0-9'] }
50
+ rule(:space?) { match['\s'].repeat }
51
+
52
+ root :addition
53
+ end
54
+ end
55
+
56
+ let(:unoptimized_calc_parser) do
57
+ Class.new(Parsanol::Parser) do
58
+ # Same rules but without optimize_rules!
59
+ rule(:addition) {
60
+ multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
61
+ multiplication
62
+ }
63
+
64
+ rule(:multiplication) {
65
+ integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
66
+ integer
67
+ }
68
+
69
+ rule(:integer) { digit.repeat(1).as(:i) >> space? }
70
+ rule(:mult_op) { match['*/'].as(:o) >> space? }
71
+ rule(:add_op) { match['+-'].as(:o) >> space? }
72
+ rule(:digit) { match['0-9'] }
73
+ rule(:space?) { match['\s'].repeat }
74
+
75
+ root :addition
76
+ end
77
+ end
78
+
79
+ let(:json_parser) do
80
+ Class.new(Parsanol::Parser) do
81
+ optimize_rules!
82
+
83
+ rule(:spaces) { match('\s').repeat(1) }
84
+ rule(:spaces?) { spaces.maybe }
85
+ rule(:comma) { spaces? >> str(',') >> spaces? }
86
+ rule(:digit) { match('[0-9]') }
87
+
88
+ rule(:number) {
89
+ (
90
+ str('-').maybe >> (
91
+ str('0') | (match('[1-9]') >> digit.repeat)
92
+ ) >> (
93
+ str('.') >> digit.repeat(1)
94
+ ).maybe >> (
95
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
96
+ ).maybe
97
+ ).as(:number)
98
+ }
99
+
100
+ rule(:string) {
101
+ str('"') >> (
102
+ str('\\') >> any | str('"').absent? >> any
103
+ ).repeat.as(:string) >> str('"')
104
+ }
105
+
106
+ rule(:array) {
107
+ str('[') >> spaces? >>
108
+ (value >> (comma >> value).repeat).maybe.as(:array) >>
109
+ spaces? >> str(']')
110
+ }
111
+
112
+ rule(:object) {
113
+ str('{') >> spaces? >>
114
+ (entry >> (comma >> entry).repeat).maybe.as(:object) >>
115
+ spaces? >> str('}')
116
+ }
117
+
118
+ rule(:value) {
119
+ string | number |
120
+ object | array |
121
+ str('true').as(:true) | str('false').as(:false) |
122
+ str('null').as(:null)
123
+ }
124
+
125
+ rule(:entry) {
126
+ (
127
+ string.as(:key) >> spaces? >>
128
+ str(':') >> spaces? >>
129
+ value.as(:val)
130
+ ).as(:entry)
131
+ }
132
+
133
+ rule(:top) { spaces? >> value >> spaces? }
134
+ root(:top)
135
+ end
136
+ end
137
+
138
+ let(:xml_parser) do
139
+ Class.new(Parsanol::Parser) do
140
+ optimize_rules!
141
+
142
+ rule(:document) {
143
+ tag(close: false).as(:o) >> document.as(:i) >> tag(close: true).as(:c) |
144
+ text
145
+ }
146
+
147
+ def tag(opts={})
148
+ close = opts[:close] || false
149
+
150
+ parslet = str('<')
151
+ parslet = parslet >> str('/') if close
152
+ parslet = parslet >> (str('>').absent? >> match("[a-zA-Z]")).repeat(1).as(:name)
153
+ parslet = parslet >> str('>')
154
+
155
+ parslet
156
+ end
157
+
158
+ rule(:text) {
159
+ match('[^<>]').repeat(0)
160
+ }
161
+
162
+ root :document
163
+ end
164
+ end
165
+
166
+ context "with optimizations enabled" do
167
+ describe "optimization safety" do
168
+ it "optimized parser does not significantly degrade performance" do
169
+ input = '1 + 2 * 3 + 4'
170
+
171
+ # Create parser instances
172
+ optimized = calc_parser.new
173
+ unoptimized = unoptimized_calc_parser.new
174
+
175
+ # Warm up
176
+ 3.times do
177
+ optimized.parse(input)
178
+ unoptimized.parse(input)
179
+ end
180
+
181
+ # Benchmark both
182
+ unoptimized_result = Benchmark.ips(quiet: true) do |x|
183
+ x.report('unoptimized') { unoptimized.parse(input) }
184
+ end
185
+
186
+ optimized_result = Benchmark.ips(quiet: true) do |x|
187
+ x.report('optimized') { optimized.parse(input) }
188
+ end
189
+
190
+ unoptimized_ips = unoptimized_result.entries.first.ips
191
+ optimized_ips = optimized_result.entries.first.ips
192
+ slowdown_ratio = optimized_ips / unoptimized_ips
193
+
194
+ # Ensure optimizer doesn't make things significantly worse
195
+ # Allow up to 20% slowdown for safety (optimizer should not harm performance)
196
+ expect(slowdown_ratio).to be >= 0.8,
197
+ "Optimizer caused significant slowdown: #{slowdown_ratio.round(2)}x " \
198
+ "(unoptimized: #{unoptimized_ips.round(0)} ips, optimized: #{optimized_ips.round(0)} ips)"
199
+ end
200
+ end
201
+
202
+ describe "baseline performance" do
203
+ it "parses calculator expressions within performance bounds" do
204
+ parser = calc_parser.new
205
+ input = '1 + 2 * 3'
206
+
207
+ result = Benchmark.ips(quiet: true) do |x|
208
+ x.report('calc') { parser.parse(input) }
209
+ end
210
+
211
+ actual_ips = result.entries.first.ips
212
+
213
+ # Allow 50% variance for different environments
214
+ min_acceptable = BASELINE_IPS[:simple_calc] * 0.5
215
+
216
+ expect(actual_ips).to be >= min_acceptable,
217
+ "Expected ≥#{min_acceptable.round(0)} ips, got #{actual_ips.round(0)} ips"
218
+ end
219
+
220
+ it "parses JSON within performance bounds" do
221
+ parser = json_parser.new
222
+ input = '{"key": "value", "array": [1,2,3]}'
223
+
224
+ result = Benchmark.ips(quiet: true) do |x|
225
+ x.report('json') { parser.parse(input) }
226
+ end
227
+
228
+ actual_ips = result.entries.first.ips
229
+
230
+ # Allow 50% variance for different environments
231
+ min_acceptable = BASELINE_IPS[:json_parse] * 0.5
232
+
233
+ expect(actual_ips).to be >= min_acceptable,
234
+ "Expected ≥#{min_acceptable.round(0)} ips, got #{actual_ips.round(0)} ips"
235
+ end
236
+
237
+ it "parses XML within performance bounds" do
238
+ parser = xml_parser.new
239
+ input = '<tag>content</tag>'
240
+
241
+ result = Benchmark.ips(quiet: true) do |x|
242
+ x.report('xml') { parser.parse(input) }
243
+ end
244
+
245
+ actual_ips = result.entries.first.ips
246
+
247
+ # Allow 50% variance for different environments
248
+ min_acceptable = BASELINE_IPS[:xml_parse] * 0.5
249
+
250
+ expect(actual_ips).to be >= min_acceptable,
251
+ "Expected ≥#{min_acceptable.round(0)} ips, got #{actual_ips.round(0)} ips"
252
+ end
253
+ end
254
+
255
+ describe "cache efficiency" do
256
+ it "maintains reasonable cache hit rate with repetition" do
257
+ parser = Class.new(Parsanol::Parser) do
258
+ optimize_rules!
259
+ rule(:digits) { match('[0-9]').repeat(3) }
260
+ root :digits
261
+ end.new
262
+
263
+ # Parse multiple times to warm cache
264
+ 10.times { parser.parse('123') }
265
+
266
+ # Get cache stats if available
267
+ if parser.respond_to?(:cache_stats)
268
+ stats = parser.cache_stats
269
+ hit_rate = stats[:hits].to_f / (stats[:hits] + stats[:misses])
270
+
271
+ expect(hit_rate).to be >= 0.05,
272
+ "Expected cache hit rate ≥5%, got #{(hit_rate * 100).round(2)}%"
273
+ else
274
+ # Skip if cache stats not available
275
+ skip "Cache stats not available in this parser"
276
+ end
277
+ end
278
+
279
+ it "keeps allocations under threshold for medium inputs" do
280
+ parser = json_parser.new
281
+ input = '{"a":1,"b":2,"c":3,"d":4,"e":5}'
282
+
283
+ # Warm up
284
+ parser.parse(input)
285
+
286
+ # Measure allocations
287
+ allocations = count_allocations { parser.parse(input) }
288
+
289
+ expect(allocations).to be < 30_000,
290
+ "Expected <30,000 allocations, got #{allocations}"
291
+ end
292
+ end
293
+ end
294
+
295
+ context "optimizer semantic equivalence" do
296
+ describe "calculator parser" do
297
+ it "produces identical parse trees with/without optimization" do
298
+ test_cases = [
299
+ '1 + 2',
300
+ '1 * 2 + 3',
301
+ '10 + 20 * 30',
302
+ '1+2+3+4+5'
303
+ ]
304
+
305
+ test_cases.each do |input|
306
+ optimized = calc_parser.new.parse(input)
307
+ unoptimized = unoptimized_calc_parser.new.parse(input)
308
+
309
+ # Strip positions for comparison
310
+ expect(strip_positions(optimized)).to eq(strip_positions(unoptimized)),
311
+ "Parse trees differ for input: #{input}"
312
+ end
313
+ end
314
+ end
315
+
316
+ describe "JSON parser" do
317
+ it "produces identical parse trees for various JSON inputs" do
318
+ test_cases = [
319
+ '{"key": "value"}',
320
+ '[1, 2, 3]',
321
+ '{"a": [1, 2], "b": {"c": 3}}',
322
+ 'null',
323
+ 'true',
324
+ 'false',
325
+ '123',
326
+ '"string"'
327
+ ]
328
+
329
+ test_cases.each do |input|
330
+ parser = json_parser.new
331
+
332
+ # Parse twice to ensure consistency
333
+ first_parse = parser.parse(input)
334
+ second_parse = json_parser.new.parse(input)
335
+
336
+ expect(strip_positions(first_parse)).to eq(strip_positions(second_parse)),
337
+ "Parse trees differ for input: #{input}"
338
+ end
339
+ end
340
+ end
341
+
342
+ describe "XML parser" do
343
+ it "produces identical parse trees for various XML inputs" do
344
+ test_cases = [
345
+ '<tag>content</tag>',
346
+ '<a><b>text</b></a>',
347
+ '<root><child>data</child></root>'
348
+ ]
349
+
350
+ test_cases.each do |input|
351
+ parser = xml_parser.new
352
+
353
+ # Parse twice to ensure consistency
354
+ first_parse = parser.parse(input)
355
+ second_parse = xml_parser.new.parse(input)
356
+
357
+ expect(strip_positions(first_parse)).to eq(strip_positions(second_parse)),
358
+ "Parse trees differ for input: #{input}"
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ # Helper methods
365
+ private
366
+
367
+ def count_allocations(&block)
368
+ GC.start
369
+ before = GC.stat(:total_allocated_objects)
370
+ block.call
371
+ after = GC.stat(:total_allocated_objects)
372
+ after - before
373
+ end
374
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'support/opal' if RUBY_ENGINE == 'opal'
4
+
5
+ require 'parsanol/parslet'
6
+ require 'parsanol/rig/rspec'
7
+ require 'parsanol/atoms/visitor'
8
+ require 'parsanol/export'
9
+
10
+ begin
11
+ require 'ae'
12
+ rescue LoadError
13
+ # AE not available
14
+ end
15
+
16
+ RSpec.configure do |config|
17
+ # Allow both old and new syntax for backward compatibility
18
+ config.expect_with :rspec do |expectations|
19
+ expectations.syntax = %i[should expect]
20
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
21
+ end
22
+
23
+ # Use rspec mocks
24
+ config.mock_with :rspec do |mocks|
25
+ mocks.verify_partial_doubles = true
26
+ end
27
+
28
+ # Shared context and examples
29
+ config.shared_context_metadata_behavior = :apply_to_host_groups
30
+
31
+ # This is not a Rails project, so no Rails-specific configuration needed
32
+
33
+ # Exclude other ruby versions by giving :ruby => '2.7' or :ruby => '3.0'
34
+ config.filter_run_excluding ruby: lambda { |version|
35
+ RUBY_VERSION !~ /^#{Regexp.escape(version.to_s)}/
36
+ }
37
+
38
+ # Exclude specs that require optional dependencies not available
39
+ begin
40
+ require 'benchmark/ips'
41
+ rescue LoadError
42
+ config.filter_run_excluding :benchmark => true
43
+ end
44
+
45
+ # Run specs in random order to surface order dependencies
46
+ config.order = :random
47
+ Kernel.srand config.seed
48
+ end
49
+
50
+ def catch_failed_parse
51
+ exception = nil
52
+ begin
53
+ yield
54
+ rescue Parsanol::ParseFailed => e
55
+ exception = e
56
+ end
57
+ exception&.parse_failure_cause
58
+ end
59
+
60
+ def slet(name, &block)
61
+ let(name, &block)
62
+ subject(&block)
63
+ end
64
+
65
+ # Helper method to convert Parsanol::Slice objects to plain strings for comparison
66
+ def strip_positions(obj)
67
+ case obj
68
+ when Parsanol::Slice
69
+ obj.to_s
70
+ when Hash
71
+ obj.transform_values { |v| strip_positions(v) }
72
+ when Array
73
+ obj.map { |item| strip_positions(item) }
74
+ when String
75
+ obj.gsub(/@\d+/, '') # Remove slice of text
76
+ else
77
+ obj
78
+ end
79
+ end
@@ -0,0 +1,8 @@
1
+ # A set of patches required to run Opal-RSpec
2
+ require 'nodejs/yaml'
3
+
4
+ module AE
5
+ # Compile in AE metadata
6
+ @metadata =
7
+ { 'revision' => 2013, 'type' => 'ruby', 'sources' => ['var'], 'authors' => [{ 'name' => 'Trans', 'email' => 'transfire@gmail.com' }], 'organizations' => [], 'requirements' => [{ 'name' => 'ansi' }, { 'groups' => ['build'], 'development' => true, 'name' => 'detroit' }, { 'groups' => ['test'], 'development' => true, 'name' => 'qed' }], 'conflicts' => [], 'alternatives' => [], 'resources' => [{ 'type' => 'home', 'uri' => 'http://rubyworks.github.com/ae', 'label' => 'Homepage' }, { 'type' => 'code', 'uri' => 'http://github.com/rubyworks/ae', 'label' => 'Source Code' }, { 'type' => 'docs', 'uri' => 'http://rubydoc.info/gems/ae', 'label' => 'Documentation' }, { 'type' => 'wiki', 'uri' => 'http://wiki.github.com/rubyworks/ae', 'label' => 'User Guide' }, { 'type' => 'bugs', 'uri' => 'http://github.com/rubyworks/ae/issues', 'label' => 'Issue Tracker' }, { 'type' => 'mail', 'uri' => 'http://groups.google.com/group/rubyworks-mailinglist', 'label' => 'Mailing List' }], 'repositories' => [{ 'name' => 'upstream', 'scm' => 'git', 'uri' => 'git://github.com/rubyworks/ae.git' }], 'categories' => [], 'copyrights' => [{ 'holder' => 'Rubyworks', 'year' => '2008', 'license' => 'BSD-2-Clause' }], 'customs' => [], 'paths' => { 'lib' => ['lib'] }, 'created' => '2008-08-17', 'summary' => 'Assertive Expressive', 'title' => 'AE', 'version' => '1.8.2', 'name' => 'ae', 'description' => "Assertive Expressive is an assertions library specifically designed \nfor reuse by other test frameworks.", 'date' => '2013-02-18' }
8
+ end
@@ -0,0 +1,14 @@
1
+ # A set of patches required to run Opal-RSpec
2
+ require 'nodejs/yaml'
3
+
4
+ module AE
5
+ # Compile in AE metadata
6
+ @metadata = (
7
+ <%=
8
+ require 'yaml'
9
+ YAML.load(
10
+ File.read(
11
+ File.expand_path('lib/ae.yml', Gem::Specification.find_by_name('ae').gem_dir)))
12
+ %>
13
+ )
14
+ end