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,248 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parsanol::Optimizers::CutInserter do
4
+ include Parsanol
5
+
6
+ let(:inserter) { Parsanol::Optimizers::CutInserter.new }
7
+
8
+ describe "basic functionality" do
9
+ it "returns parslet unchanged if not an alternative" do
10
+ parslet = str('hello')
11
+ result = inserter.optimize(parslet)
12
+ expect(result.to_s).to include('hello')
13
+ end
14
+
15
+ it "returns sequence unchanged if no alternatives inside" do
16
+ parslet = str('a') >> str('b')
17
+ result = inserter.optimize(parslet)
18
+ # Note: Phase 24 may optimize this to str('ab')
19
+ expect(result).to be_a(Parsanol::Atoms::Base)
20
+ end
21
+ end
22
+
23
+ describe "disjoint alternatives" do
24
+ it "inserts cuts for simple disjoint alternatives" do
25
+ parslet = str('if') | str('while') | str('print')
26
+ result = inserter.optimize(parslet)
27
+
28
+ # Should insert cuts in each alternative
29
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
30
+ result.alternatives.each do |alt|
31
+ # Each alternative should be wrapped with cut
32
+ expect(alt).to be_a(Parsanol::Atoms::Cut)
33
+ end
34
+ end
35
+
36
+ it "inserts cuts for two disjoint alternatives" do
37
+ parslet = str('yes') | str('no')
38
+ result = inserter.optimize(parslet)
39
+
40
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
41
+ expect(result.alternatives.size).to eq(2)
42
+ result.alternatives.each do |alt|
43
+ expect(alt).to be_a(Parsanol::Atoms::Cut)
44
+ end
45
+ end
46
+
47
+ it "inserts cuts after deterministic prefix in sequences" do
48
+ # Note: Phase 24 may concatenate adjacent strings
49
+ # So str('if') >> str(' ') >> str('x') may become str('if x')
50
+ parslet = (str('if') >> str(' ') >> str('x')) |
51
+ (str('while') >> str(' ') >> str('y'))
52
+ result = inserter.optimize(parslet)
53
+
54
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
55
+ # Each alternative should be wrapped with cut
56
+ # (May be Cut wrapping whole thing if Phase 24 concatenated strings)
57
+ result.alternatives.each do |alt|
58
+ expect(alt).to be_a(Parsanol::Atoms::Cut)
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "overlapping alternatives" do
64
+ it "does not insert cuts when FIRST sets overlap" do
65
+ # Same atom in both alternatives - not disjoint
66
+ atom = str('same')
67
+ parslet = atom | atom
68
+ result = inserter.optimize(parslet)
69
+
70
+ # Should not insert cuts
71
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
72
+ result.alternatives.each do |alt|
73
+ expect(alt).not_to be_a(Parsanol::Atoms::Cut)
74
+ end
75
+ end
76
+ end
77
+
78
+ describe "EPSILON handling" do
79
+ it "does not cut after parslets with EPSILON in FIRST set" do
80
+ # str('a').maybe has EPSILON in FIRST set
81
+ parslet = (str('a').maybe >> str('b')) |
82
+ (str('c') >> str('d'))
83
+ result = inserter.optimize(parslet)
84
+
85
+ # First alternative has EPSILON, so shouldn't have cut at start
86
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
87
+ first_alt = result.alternatives[0]
88
+ # The sequence may be optimized but should still have structure
89
+ # Second alternative should be wrapped with cut since 'cd' is deterministic
90
+ second_alt = result.alternatives[1]
91
+ expect(second_alt).to be_a(Parsanol::Atoms::Cut)
92
+ end
93
+
94
+ it "cuts after non-EPSILON prefix even if later elements have EPSILON" do
95
+ # str('if') doesn't have EPSILON, safe to cut after it
96
+ parslet = (str('if') >> str('x').maybe >> str('y')) |
97
+ (str('while') >> str('z'))
98
+ result = inserter.optimize(parslet)
99
+
100
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
101
+ # First alternative should have cut after 'if'
102
+ first_alt = result.alternatives[0]
103
+ expect(first_alt).to be_a(Parsanol::Atoms::Sequence)
104
+ # First element should be a cut wrapping 'if'
105
+ expect(first_alt.parslets.first).to be_a(Parsanol::Atoms::Cut)
106
+ end
107
+ end
108
+
109
+ describe "nested alternatives" do
110
+ it "recursively optimizes nested alternatives" do
111
+ # Outer alternative: 'a' | (inner alternative)
112
+ # Inner alternative: 'b' | 'c'
113
+ inner = str('b') | str('c')
114
+ outer = str('a') | inner
115
+
116
+ result = inserter.optimize(outer)
117
+
118
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
119
+ # Outer should have 2 alternatives
120
+ expect(result.alternatives.size).to eq(2)
121
+
122
+ # First should be wrapped with cut
123
+ expect(result.alternatives[0]).to be_a(Parsanol::Atoms::Cut)
124
+
125
+ # Second alternative should be the optimized inner alternative
126
+ # It will be a Cut-wrapped Alternative
127
+ second = result.alternatives[1]
128
+ # The inner alternative was optimized and wrapped
129
+ # Structure: outer sees flattened alternatives OR nested structure
130
+ # Just verify both alternatives have cuts applied somewhere
131
+ expect(result.to_s).to include('↑')
132
+ end
133
+ end
134
+
135
+ describe "repetitions" do
136
+ it "recursively optimizes alternatives inside repetitions" do
137
+ parslet = (str('a') | str('b')).repeat(1, 3)
138
+ result = inserter.optimize(parslet)
139
+
140
+ expect(result).to be_a(Parsanol::Atoms::Repetition)
141
+ # The inner alternative should be optimized
142
+ inner = result.parslet
143
+ expect(inner).to be_a(Parsanol::Atoms::Alternative)
144
+ inner.alternatives.each do |alt|
145
+ expect(alt).to be_a(Parsanol::Atoms::Cut)
146
+ end
147
+ end
148
+ end
149
+
150
+ describe "named atoms" do
151
+ it "recursively optimizes named alternatives" do
152
+ parslet = (str('x') | str('y')).as(:choice)
153
+ result = inserter.optimize(parslet)
154
+
155
+ expect(result).to be_a(Parsanol::Atoms::Named)
156
+ expect(result.name).to eq(:choice)
157
+
158
+ # The wrapped alternative should be optimized
159
+ inner = result.parslet
160
+ expect(inner).to be_a(Parsanol::Atoms::Alternative)
161
+ inner.alternatives.each do |alt|
162
+ expect(alt).to be_a(Parsanol::Atoms::Cut)
163
+ end
164
+ end
165
+ end
166
+
167
+ describe "complex grammars" do
168
+ it "optimizes statement-like grammar" do
169
+ # Simulates: if_stmt | while_stmt | print_stmt
170
+ # Each statement has keyword followed by other stuff
171
+ # Note: Phase 24 may concatenate these into single strings
172
+ if_stmt = str('if') >> str(' ') >> str('condition')
173
+ while_stmt = str('while') >> str(' ') >> str('condition')
174
+ print_stmt = str('print') >> str(' ') >> str('expr')
175
+
176
+ parslet = if_stmt | while_stmt | print_stmt
177
+ result = inserter.optimize(parslet)
178
+
179
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
180
+ expect(result.alternatives.size).to eq(3)
181
+
182
+ # Each alternative should have a cut somewhere
183
+ # The exact structure depends on Phase 24 optimization
184
+ result.alternatives.each do |alt|
185
+ # Should be either Cut or Sequence with Cut
186
+ expect([Parsanol::Atoms::Cut, Parsanol::Atoms::Sequence]).to include(alt.class)
187
+ end
188
+ end
189
+
190
+ it "handles mixed safe and unsafe alternatives correctly" do
191
+ # If FIRST sets aren't all disjoint, no cuts
192
+ parslet = str('a') >> str('b') |
193
+ str('a') >> str('c') # Both start with 'a' - not disjoint!
194
+
195
+ result = inserter.optimize(parslet)
196
+
197
+ # Should not insert cuts since FIRST sets overlap
198
+ # Note: str('a') >> str('b') might be optimized to str('ab') by Phase 24
199
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
200
+ end
201
+ end
202
+
203
+ describe "edge cases" do
204
+ it "handles single alternative (no optimization needed)" do
205
+ # Alternative with one option - trivial case
206
+ parslet = Parsanol::Atoms::Alternative.new(str('only'))
207
+ result = inserter.optimize(parslet)
208
+
209
+ # Should still work, just return optimized single alternative
210
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
211
+ end
212
+
213
+ it "handles empty sequence prefix (no cut insertion)" do
214
+ # If prefix is empty, shouldn't insert cut
215
+ parslet = (str('a').maybe >> str('b')) | str('c')
216
+ result = inserter.optimize(parslet)
217
+
218
+ # Alternatives are disjoint but first has EPSILON prefix
219
+ expect(result).to be_a(Parsanol::Atoms::Alternative)
220
+ end
221
+ end
222
+
223
+ describe "preservation of semantics" do
224
+ it "produces parslet that parses the same input" do
225
+ parslet = str('if') | str('while') | str('for')
226
+ optimized = inserter.optimize(parslet)
227
+
228
+ # Should parse same inputs
229
+ expect(parslet.parse('if')).to eq('if')
230
+ expect(optimized.parse('if')).to eq('if')
231
+
232
+ expect(parslet.parse('while')).to eq('while')
233
+ expect(optimized.parse('while')).to eq('while')
234
+
235
+ expect(parslet.parse('for')).to eq('for')
236
+ expect(optimized.parse('for')).to eq('for')
237
+ end
238
+
239
+ it "produces parslet that fails on same invalid input" do
240
+ parslet = str('yes') | str('no')
241
+ optimized = inserter.optimize(parslet)
242
+
243
+ # Both should fail on invalid input
244
+ expect { parslet.parse('maybe') }.to raise_error(Parsanol::ParseFailed)
245
+ expect { optimized.parse('maybe') }.to raise_error(Parsanol::ParseFailed)
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Cut operator" do
4
+ include Parsanol
5
+
6
+ describe "basic functionality" do
7
+ it "allows successful parse when cut succeeds" do
8
+ parser = str('if').cut >> str(' ') >> str('x')
9
+ result = parser.parse('if x')
10
+ expect(result).to eq('if x')
11
+ end
12
+
13
+ it "provides cache eviction on successful cut" do
14
+ # Cut operator primarily provides cache eviction for memory optimization
15
+ # It clears cache before the cut position
16
+ parser = str('if').cut >> str('x')
17
+ result = parser.parse('ifx')
18
+ expect(result).to eq('ifx')
19
+ end
20
+ end
21
+
22
+ describe "with alternatives" do
23
+ it "works with cuts in each alternative branch" do
24
+ parser =
25
+ (str('if').cut >> str(' then')) |
26
+ (str('while').cut >> str(' do')) |
27
+ str('print')
28
+
29
+ expect(parser.parse('if then')).to eq('if then')
30
+ expect(parser.parse('while do')).to eq('while do')
31
+ expect(parser.parse('print')).to eq('print')
32
+ end
33
+ end
34
+
35
+ describe "cut position" do
36
+ it "cuts at the correct position" do
37
+ parser = str('a').cut >> str('b') >> str('c')
38
+ result = parser.parse('abc')
39
+ expect(result).to eq('abc')
40
+ end
41
+ end
42
+
43
+ describe "FIRST set delegation" do
44
+ it "delegates first_set to wrapped parslet" do
45
+ cut_atom = str('test').cut
46
+ expect(cut_atom.first_set.size).to eq(1)
47
+ expect(cut_atom.first_set.first).to be_a(Parsanol::Atoms::Str)
48
+ expect(cut_atom.first_set.first.str).to eq('test')
49
+ end
50
+ end
51
+
52
+ describe "caching behavior" do
53
+ it "is not cached itself (thin wrapper)" do
54
+ cut_atom = str('test').cut
55
+ expect(cut_atom.cached?).to be false
56
+ end
57
+ end
58
+
59
+ describe "string representation" do
60
+ it "shows cut operator in to_s" do
61
+ cut_atom = str('foo').cut
62
+ expect(cut_atom.to_s).to include('foo')
63
+ expect(cut_atom.to_s).to include('↑')
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,218 @@
1
+ require 'spec_helper'
2
+ require 'parsanol/edit_tracker'
3
+
4
+ describe Parsanol::EditTracker do
5
+ let(:tracker) { Parsanol::EditTracker.new }
6
+
7
+ describe '#initialize' do
8
+ it 'creates an empty tracker' do
9
+ expect(tracker.empty?).to be true
10
+ expect(tracker.size).to eq 0
11
+ end
12
+ end
13
+
14
+ describe '#insert and #delete' do
15
+ it 'records insertions' do
16
+ tracker.insert(10, 5)
17
+ expect(tracker.size).to eq 1
18
+ expect(tracker.edits.first.position).to eq 10
19
+ expect(tracker.edits.first.delta).to eq 5
20
+ end
21
+
22
+ it 'records deletions' do
23
+ tracker.delete(20, 3)
24
+ expect(tracker.size).to eq 1
25
+ expect(tracker.edits.first.position).to eq 20
26
+ expect(tracker.edits.first.delta).to eq(-3)
27
+ end
28
+
29
+ it 'records multiple edits in order' do
30
+ tracker.insert(10, 5)
31
+ tracker.delete(30, 2)
32
+ tracker.insert(50, 10)
33
+
34
+ expect(tracker.size).to eq 3
35
+ expect(tracker.edits[0].delta).to eq 5
36
+ expect(tracker.edits[1].delta).to eq(-2)
37
+ expect(tracker.edits[2].delta).to eq 10
38
+ end
39
+ end
40
+
41
+ describe '#shift_interval' do
42
+ context 'with no edits' do
43
+ it 'returns the original interval' do
44
+ result = tracker.shift_interval(10, 20)
45
+ expect(result).to eq [10, 20]
46
+ end
47
+ end
48
+
49
+ context 'with edit before interval' do
50
+ it 'shifts both boundaries forward for insertion' do
51
+ tracker.insert(5, 10) # Insert 10 chars at position 5
52
+ result = tracker.shift_interval(10, 20)
53
+ expect(result).to eq [20, 30] # Both shifted by +10
54
+ end
55
+
56
+ it 'shifts both boundaries backward for deletion' do
57
+ tracker.delete(5, 3) # Delete 3 chars at position 5
58
+ result = tracker.shift_interval(10, 20)
59
+ expect(result).to eq [7, 17] # Both shifted by -3
60
+ end
61
+ end
62
+
63
+ context 'with edit after interval' do
64
+ it 'does not shift interval for insertion' do
65
+ tracker.insert(50, 10)
66
+ result = tracker.shift_interval(10, 20)
67
+ expect(result).to eq [10, 20] # No change
68
+ end
69
+
70
+ it 'does not shift interval for deletion' do
71
+ tracker.delete(50, 5)
72
+ result = tracker.shift_interval(10, 20)
73
+ expect(result).to eq [10, 20] # No change
74
+ end
75
+ end
76
+
77
+ context 'with edit inside interval' do
78
+ it 'invalidates interval for insertion' do
79
+ tracker.insert(15, 5) # Insert inside [10, 20)
80
+ result = tracker.shift_interval(10, 20)
81
+ expect(result).to be_nil # Invalidated
82
+ end
83
+
84
+ it 'invalidates interval for deletion' do
85
+ tracker.delete(15, 3) # Delete inside [10, 20)
86
+ result = tracker.shift_interval(10, 20)
87
+ expect(result).to be_nil # Invalidated
88
+ end
89
+
90
+ it 'invalidates interval for edit at start boundary' do
91
+ tracker.insert(10, 5) # Edit at start of [10, 20)
92
+ result = tracker.shift_interval(10, 20)
93
+ expect(result).to be_nil # Invalidated
94
+ end
95
+ end
96
+
97
+ context 'with edit at end boundary' do
98
+ it 'does not invalidate interval' do
99
+ tracker.insert(20, 5) # Edit at end of [10, 20) - not inside
100
+ result = tracker.shift_interval(10, 20)
101
+ expect(result).to eq [10, 20] # Not invalidated
102
+ end
103
+ end
104
+
105
+ context 'with multiple edits' do
106
+ it 'applies all edits in order' do
107
+ tracker.insert(5, 10) # Shift [10,20) to [20,30)
108
+ tracker.insert(3, 5) # Shift [20,30) to [25,35)
109
+ tracker.delete(2, 1) # Shift [25,35) to [24,34)
110
+
111
+ result = tracker.shift_interval(10, 20)
112
+ expect(result).to eq [24, 34]
113
+ end
114
+
115
+ it 'invalidates if any edit is inside interval' do
116
+ tracker.insert(5, 10) # Shift [10,20) to [20,30)
117
+ tracker.insert(25, 5) # Inside shifted interval [20,30)
118
+
119
+ result = tracker.shift_interval(10, 20)
120
+ expect(result).to be_nil # Invalidated by second edit
121
+ end
122
+
123
+ it 'handles complex sequence of edits' do
124
+ # Start with interval [100, 200)
125
+ tracker.insert(50, 20) # Shift to [120, 220)
126
+ tracker.delete(80, 10) # Shift to [110, 210)
127
+ tracker.insert(250, 30) # After interval, no shift
128
+ tracker.insert(90, 5) # Shift to [115, 215)
129
+
130
+ result = tracker.shift_interval(100, 200)
131
+ expect(result).to eq [115, 215]
132
+ end
133
+ end
134
+
135
+ context 'with invalidation conditions' do
136
+ it 'invalidates if shifted interval becomes negative' do
137
+ tracker.delete(5, 20) # Large deletion before interval
138
+ result = tracker.shift_interval(10, 20)
139
+ expect(result).to be_nil # Would become [<0, <0)
140
+ end
141
+
142
+ it 'invalidates if high becomes less than low' do
143
+ tracker.delete(5, 100) # Very large deletion
144
+ result = tracker.shift_interval(10, 20)
145
+ expect(result).to be_nil # Would become invalid
146
+ end
147
+ end
148
+ end
149
+
150
+ describe '#invalidates?' do
151
+ it 'returns false when interval is valid after edits' do
152
+ tracker.insert(5, 10)
153
+ expect(tracker.invalidates?(20, 30)).to be false
154
+ end
155
+
156
+ it 'returns true when interval is invalidated' do
157
+ tracker.insert(15, 5)
158
+ expect(tracker.invalidates?(10, 20)).to be true
159
+ end
160
+ end
161
+
162
+ describe '#clear' do
163
+ it 'removes all edits' do
164
+ tracker.insert(10, 5)
165
+ tracker.delete(20, 3)
166
+ tracker.clear
167
+
168
+ expect(tracker.empty?).to be true
169
+ expect(tracker.size).to eq 0
170
+ end
171
+
172
+ it 'resets interval shifting' do
173
+ tracker.insert(5, 10)
174
+ tracker.clear
175
+
176
+ result = tracker.shift_interval(10, 20)
177
+ expect(result).to eq [10, 20] # No shift after clear
178
+ end
179
+ end
180
+
181
+ describe 'edge cases' do
182
+ it 'handles zero-length intervals' do
183
+ tracker.insert(10, 5)
184
+ result = tracker.shift_interval(15, 15)
185
+ expect(result).to eq [20, 20] # Shifted but still zero-length
186
+ end
187
+
188
+ it 'handles zero-length insertions' do
189
+ tracker.insert(10, 0)
190
+ result = tracker.shift_interval(5, 15)
191
+ expect(result).to eq [5, 15] # No change
192
+ end
193
+
194
+ it 'handles zero-length deletions' do
195
+ tracker.delete(10, 0)
196
+ result = tracker.shift_interval(5, 15)
197
+ expect(result).to eq [5, 15] # No change
198
+ end
199
+
200
+ it 'handles negative positions in edits' do
201
+ tracker.insert(-5, 10)
202
+ result = tracker.shift_interval(10, 20)
203
+ expect(result).to eq [20, 30] # Shifted
204
+ end
205
+ end
206
+
207
+ describe 'Edit#to_s' do
208
+ it 'describes insertions' do
209
+ edit = Parsanol::EditTracker::Edit.new(10, 5)
210
+ expect(edit.to_s).to eq "Insert(5 chars at 10)"
211
+ end
212
+
213
+ it 'describes deletions' do
214
+ edit = Parsanol::EditTracker::Edit.new(20, -3)
215
+ expect(edit.to_s).to eq "Delete(3 chars at 20)"
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parsanol::ErrorReporter::Contextual do
4
+ let(:reporter) { described_class.new }
5
+ let(:fake_source) { double('source') }
6
+ let(:fake_atom) { double('atom') }
7
+ let(:fake_cause) { double('cause') }
8
+
9
+ describe '#err' do
10
+ before do
11
+ allow(fake_source).to receive(:pos).and_return(13)
12
+ allow(fake_source).to receive(:line_and_column).and_return([1, 1])
13
+ end
14
+
15
+ it 'returns the deepest cause' do
16
+ expect(reporter).to receive(:deepest).and_return(:deepest)
17
+ expect(reporter.err('parslet', fake_source, 'message')).to eq(:deepest)
18
+ end
19
+ end
20
+
21
+ describe '#err_at' do
22
+ before do
23
+ allow(fake_source).to receive(:pos).and_return(13)
24
+ allow(fake_source).to receive(:line_and_column).and_return([1, 1])
25
+ end
26
+
27
+ it 'returns the deepest cause' do
28
+ expect(reporter).to receive(:deepest).and_return(:deepest)
29
+ expect(reporter.err('parslet', fake_source, 'message', 13)).to eq(:deepest)
30
+ end
31
+ end
32
+
33
+ describe '#deepest(cause)' do
34
+ def fake_cause(pos = 13, children = nil)
35
+ double('cause' + pos.to_s, pos: pos, children: children)
36
+ end
37
+
38
+ context 'when there is no deepest cause yet' do
39
+ let(:cause) { fake_cause }
40
+
41
+ it 'returns the given cause' do
42
+ reporter.deepest(cause).should == cause
43
+ end
44
+ end
45
+
46
+ context 'when the previous cause is deeper (no relationship)' do
47
+ let(:previous) { fake_cause }
48
+
49
+ before do
50
+ reporter.deepest(previous)
51
+ end
52
+
53
+ it 'returns the previous cause' do
54
+ reporter.deepest(fake_cause(12))
55
+ .should == previous
56
+ end
57
+ end
58
+
59
+ context 'when the previous cause is deeper (child)' do
60
+ let(:previous) { fake_cause }
61
+
62
+ before do
63
+ reporter.deepest(previous)
64
+ end
65
+
66
+ it 'returns the given cause' do
67
+ given = fake_cause(12, [previous])
68
+ reporter.deepest(given).should == given
69
+ end
70
+ end
71
+
72
+ context 'when the previous cause is shallower' do
73
+ before do
74
+ reporter.deepest(fake_cause)
75
+ end
76
+
77
+ it 'stores the cause as deepest' do
78
+ deeper = fake_cause(14)
79
+ reporter.deepest(deeper)
80
+ reporter.deepest_cause.should == deeper
81
+ end
82
+ end
83
+ end
84
+
85
+ describe '#reset' do
86
+ before do
87
+ allow(fake_source).to receive(:pos).and_return(Parsanol::Position.new(
88
+ "source", 13, 13
89
+ ))
90
+ allow(fake_source).to receive(:line_and_column).and_return([1, 1])
91
+ end
92
+
93
+ it 'resets deepest cause on success of sibling expression' do
94
+ expect(reporter).to receive(:deepest).and_return(:deepest)
95
+ expect(reporter.err('parslet', fake_source, 'message')).to eq(:deepest)
96
+ expect(reporter).to receive(:reset).once
97
+ reporter.succ(fake_source)
98
+ end
99
+ end
100
+
101
+ describe 'label' do
102
+ before do
103
+ allow(fake_source).to receive(:pos).and_return(Parsanol::Position.new(
104
+ "source", 13, 13
105
+ ))
106
+ allow(fake_source).to receive(:line_and_column).and_return([1, 1])
107
+ end
108
+
109
+ it 'sets label if atom has one' do
110
+ expect(fake_atom).to receive(:label).once.and_return('label')
111
+ expect(fake_cause).to receive(:set_label).once
112
+ expect(reporter).to receive(:deepest).and_return(fake_cause)
113
+ expect(reporter.err(fake_atom, fake_source, 'message')).to eq(fake_cause)
114
+ end
115
+
116
+ it 'does not set label if atom does not have one' do
117
+ expect(reporter).to receive(:deepest).and_return(:deepest)
118
+ expect(fake_atom).not_to receive(:update_label)
119
+ expect(reporter.err(fake_atom, fake_source, 'message')).to eq(:deepest)
120
+ end
121
+ end
122
+ end