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,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'json'
5
+
6
+ describe Parsanol::Serialized do
7
+ let(:parser_class) do
8
+ Class.new(Parsanol::Parser) do
9
+ include Parsanol::Serialized
10
+
11
+ rule(:number) { match('[0-9]').repeat(1).as(:int) }
12
+ rule(:space) { match('\s').repeat }
13
+ rule(:add_op) { match('[+-]').as(:op) >> space }
14
+ rule(:expression) { (number.as(:left) >> add_op.as(:op) >> expression.as(:right)).as(:binop) | number }
15
+ root(:expression)
16
+ end
17
+ end
18
+
19
+ let(:parser) { parser_class.new }
20
+
21
+ describe '#parse_to_json' do
22
+ context 'when native extension is not available' do
23
+ before do
24
+ allow(Parsanol::Native).to receive(:available?).and_return(false)
25
+ end
26
+
27
+ it 'raises LoadError' do
28
+ expect { parser.parse_to_json('42') }.to raise_error(LoadError, /Serialized mode requires native extension/)
29
+ end
30
+ end
31
+ end
32
+
33
+ describe '#parse_to_struct' do
34
+ context 'when native extension is not available' do
35
+ before do
36
+ allow(Parsanol::Native).to receive(:available?).and_return(false)
37
+ end
38
+
39
+ it 'raises LoadError' do
40
+ deserializer = Class.new do
41
+ def self.from_json(json); JSON.parse(json); end
42
+ end
43
+ expect { parser.parse_to_struct('42', deserializer) }.to raise_error(LoadError)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '#parse' do
49
+ context 'when native extension is not available' do
50
+ before do
51
+ allow(Parsanol::Native).to receive(:available?).and_return(false)
52
+ end
53
+
54
+ it 'raises LoadError' do
55
+ expect { parser.parse('42') }.to raise_error(LoadError, /Serialized mode requires native extension/)
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '.output_schema' do
61
+ it 'allows defining output schema' do
62
+ parser_class.output_schema(
63
+ number: { type: :integer },
64
+ binop: { type: :object, properties: [:left, :op, :right] }
65
+ )
66
+ expect(parser_class.output_schema).to be_a(Hash)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Parsanol::ZeroCopy do
6
+ # Define test AST classes
7
+ before(:all) do
8
+ module TestCalculator
9
+ class Expr
10
+ def eval
11
+ raise NotImplementedError
12
+ end
13
+ end
14
+
15
+ class Number < Expr
16
+ attr_reader :value
17
+
18
+ def initialize(value)
19
+ @value = value
20
+ end
21
+
22
+ def eval = @value
23
+ end
24
+
25
+ class BinOp < Expr
26
+ attr_reader :left, :op, :right
27
+
28
+ def initialize(left:, op:, right:)
29
+ @left = left
30
+ @op = op
31
+ @right = right
32
+ end
33
+
34
+ def eval
35
+ case @op
36
+ when '+' then @left.eval + @right.eval
37
+ when '-' then @left.eval - @right.eval
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ let(:parser_class) do
45
+ Class.new(Parsanol::Parser) do
46
+ include Parsanol::ZeroCopy
47
+
48
+ rule(:number) { match('[0-9]').repeat(1).as(:int) }
49
+ rule(:space) { match('\s').repeat }
50
+ rule(:add_op) { match('[+-]').as(:op) >> space }
51
+ rule(:expression) { (number.as(:left) >> add_op.as(:op) >> expression.as(:right)).as(:binop) | number }
52
+ root(:expression)
53
+ end
54
+ end
55
+
56
+ let(:parser) { parser_class.new }
57
+
58
+ describe '.output_types' do
59
+ it 'allows defining output types' do
60
+ parser_class.output_types(
61
+ number: TestCalculator::Number,
62
+ binop: TestCalculator::BinOp
63
+ )
64
+ expect(parser_class.output_types[:number]).to eq(TestCalculator::Number)
65
+ expect(parser_class.output_types[:binop]).to eq(TestCalculator::BinOp)
66
+ end
67
+
68
+ it 'returns empty hash by default' do
69
+ fresh_parser_class = Class.new(Parsanol::Parser) do
70
+ include Parsanol::ZeroCopy
71
+ end
72
+ expect(fresh_parser_class.output_types).to eq({})
73
+ end
74
+ end
75
+
76
+ describe '.output_type' do
77
+ it 'allows defining a single output type' do
78
+ parser_class.output_type(:number, TestCalculator::Number)
79
+ expect(parser_class.output_types[:number]).to eq(TestCalculator::Number)
80
+ end
81
+ end
82
+
83
+ describe '.output_types_for_ffi' do
84
+ it 'converts types to FFI-compatible format' do
85
+ parser_class.output_types(
86
+ number: TestCalculator::Number,
87
+ binop: TestCalculator::BinOp
88
+ )
89
+ ffi_types = parser_class.output_types_for_ffi
90
+ expect(ffi_types['number']).to eq('TestCalculator::Number')
91
+ expect(ffi_types['binop']).to eq('TestCalculator::BinOp')
92
+ end
93
+ end
94
+
95
+ describe '#parse' do
96
+ context 'when native extension is not available' do
97
+ before do
98
+ allow(Parsanol::Native).to receive(:available?).and_return(false)
99
+ end
100
+
101
+ it 'raises LoadError' do
102
+ expect { parser.parse('42') }.to raise_error(LoadError, /ZeroCopy mode requires native extension/)
103
+ end
104
+ end
105
+
106
+ context 'when output_types is not defined' do
107
+ before do
108
+ allow(Parsanol::Native).to receive(:available?).and_return(true)
109
+ allow(Parsanol::Native).to receive(:serialize_grammar).and_return('{}')
110
+ end
111
+
112
+ it 'raises ArgumentError' do
113
+ fresh_parser_class = Class.new(Parsanol::Parser) do
114
+ include Parsanol::ZeroCopy
115
+ rule(:test) { str('a') }
116
+ root(:test)
117
+ end
118
+ fresh_parser = fresh_parser_class.new
119
+ expect { fresh_parser.parse('a') }.to raise_error(ArgumentError, /ZeroCopy mode requires output_types/)
120
+ end
121
+ end
122
+ end
123
+
124
+ describe '#parse_with_types' do
125
+ context 'when native extension is not available' do
126
+ before do
127
+ allow(Parsanol::Native).to receive(:available?).and_return(false)
128
+ end
129
+
130
+ it 'raises LoadError' do
131
+ expect { parser.parse_with_types('42', {}) }.to raise_error(LoadError, /ZeroCopy mode requires native extension/)
132
+ end
133
+ end
134
+ end
135
+
136
+ # Tests for parse_to_ruby_objects FFI function
137
+ # These tests verify that the new FFI function directly constructs
138
+ # Parsanol::Slice objects without intermediate Hash markers.
139
+ describe 'parse_to_ruby_objects FFI' do
140
+ # Simple string match grammar
141
+ let(:string_grammar) do
142
+ {
143
+ atoms: [{ Str: { pattern: 'hello' } }],
144
+ root: 0
145
+ }.to_json
146
+ end
147
+
148
+ # Named capture grammar
149
+ let(:named_grammar) do
150
+ {
151
+ atoms: [
152
+ { Str: { pattern: 'hello' } },
153
+ { Named: { name: 'greeting', atom: 0 } }
154
+ ],
155
+ root: 1
156
+ }.to_json
157
+ end
158
+
159
+ # Sequence grammar
160
+ let(:sequence_grammar) do
161
+ {
162
+ atoms: [
163
+ { Str: { pattern: 'hello' } },
164
+ { Str: { pattern: ' ' } },
165
+ { Str: { pattern: 'world' } },
166
+ { Sequence: { atoms: [0, 1, 2] } }
167
+ ],
168
+ root: 3
169
+ }.to_json
170
+ end
171
+
172
+ context 'with native extension available' do
173
+ before do
174
+ skip 'Native extension not available' unless Parsanol::Native.available?
175
+ end
176
+
177
+ it 'returns Slice object for simple string match' do
178
+ result = Parsanol::Native.parse_to_ruby_objects(string_grammar, 'hello')
179
+ expect(result).to be_a(Parsanol::Slice)
180
+ expect(result.to_s).to eq('hello')
181
+ expect(result.offset).to eq(0)
182
+ end
183
+
184
+ it 'returns Hash with Slice values for named captures' do
185
+ result = Parsanol::Native.parse_to_ruby_objects(named_grammar, 'hello')
186
+ expect(result).to be_a(Hash)
187
+ expect(result.keys).to eq(['greeting'])
188
+
189
+ greeting = result['greeting']
190
+ expect(greeting).to be_a(Parsanol::Slice)
191
+ expect(greeting.to_s).to eq('hello')
192
+ expect(greeting.offset).to eq(0)
193
+ end
194
+
195
+ it 'returns Array of Slice objects for sequences' do
196
+ result = Parsanol::Native.parse_to_ruby_objects(sequence_grammar, 'hello world')
197
+ expect(result).to be_a(Array)
198
+ expect(result.length).to eq(3)
199
+
200
+ # First slice: "hello"
201
+ expect(result[0]).to be_a(Parsanol::Slice)
202
+ expect(result[0].to_s).to eq('hello')
203
+ expect(result[0].offset).to eq(0)
204
+
205
+ # Second slice: " "
206
+ expect(result[1]).to be_a(Parsanol::Slice)
207
+ expect(result[1].to_s).to eq(' ')
208
+ expect(result[1].offset).to eq(5)
209
+
210
+ # Third slice: "world"
211
+ expect(result[2]).to be_a(Parsanol::Slice)
212
+ expect(result[2].to_s).to eq('world')
213
+ expect(result[2].offset).to eq(6)
214
+ end
215
+
216
+ it 'preserves correct byte offsets for multi-byte characters' do
217
+ # Test with input containing UTF-8 characters
218
+ utf8_grammar = {
219
+ atoms: [{ Str: { pattern: '日本語' } }],
220
+ root: 0
221
+ }.to_json
222
+
223
+ result = Parsanol::Native.parse_to_ruby_objects(utf8_grammar, '日本語')
224
+ expect(result).to be_a(Parsanol::Slice)
225
+ expect(result.to_s).to eq('日本語')
226
+ expect(result.offset).to eq(0)
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parsanol::Parser do
4
+ include Parsanol
5
+ class FooParser < Parsanol::Parser
6
+ rule(:foo) { str('foo') }
7
+ root(:foo)
8
+ end
9
+
10
+ describe '<- .root' do
11
+ parser = Class.new(Parsanol::Parser) do
12
+ def root_parslet
13
+ :answer
14
+ end
15
+ end
16
+ parser.root :root_parslet
17
+
18
+ it "has defined a 'root' method, returning the root" do
19
+ parser_instance = parser.new
20
+ expect(parser_instance.root).to eq(:answer)
21
+ end
22
+ end
23
+
24
+ it "parses 'foo'" do
25
+ FooParser.new.parse('foo').should == 'foo'
26
+ end
27
+
28
+ context 'composition' do
29
+ let(:parser) { FooParser.new }
30
+
31
+ it 'allows concatenation' do
32
+ composite = parser >> str('bar')
33
+ composite.should parse('foobar')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parsanol do
4
+ include Parsanol
5
+
6
+ describe Parsanol::ParseFailed do
7
+ it "should be caught by an empty rescue" do
8
+ begin
9
+ raise Parsanol::ParseFailed
10
+ rescue
11
+ # Success! Ignore this.
12
+ end
13
+ end
14
+ end
15
+ describe "<- .rule" do
16
+ # Rules define methods. This can be easily tested by defining them right
17
+ # here.
18
+ context "empty rule" do
19
+ rule(:empty) { }
20
+
21
+ it "should raise a NotImplementedError" do
22
+ lambda {
23
+ empty.parslet
24
+ }.should raise_error(NotImplementedError)
25
+ end
26
+ end
27
+
28
+ context "containing 'any'" do
29
+ rule(:any_rule) { any }
30
+ subject { any_rule }
31
+
32
+ it { should be_a Parsanol::Atoms::Entity }
33
+ it "should memoize the returned instance" do
34
+ any_rule.object_id.should == any_rule.object_id
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,272 @@
1
+ require 'spec_helper'
2
+
3
+ require 'parsanol/parslet'
4
+
5
+ describe Parsanol::Pattern do
6
+ include Parsanol
7
+
8
+ # These two factory methods help make the specs more robust to interface
9
+ # changes. They also help to label trees (t) and patterns (p).
10
+ def p(pattern)
11
+ Parsanol::Pattern.new(pattern)
12
+ end
13
+ def t(obj)
14
+ obj
15
+ end
16
+
17
+ # Tries to match pattern to the tree, and verifies the bindings hash. Don't
18
+ # use this for new examples.
19
+ #
20
+ RSpec::Matchers.define :match_with_bind do |pattern, exp_bindings|
21
+ unless respond_to?(:failure_message)
22
+ alias_method :failure_message_for_should, :failure_message
23
+ end
24
+
25
+ failure_message do |tree|
26
+ "expected #{pattern.inspect} to match #{tree.inspect}, but didn't. (block wasn't called or not correctly)"
27
+ end
28
+ match do |tree|
29
+ bindings = Parsanol::Pattern.new(pattern).match(tree)
30
+ bindings && bindings == exp_bindings
31
+ end
32
+ end
33
+
34
+ # This is the more modern version of verifying a match: (uses 'exp'
35
+ # implicitly). Checks for a match of pattern in +exp+ and yields the
36
+ # matched variables.
37
+ #
38
+ def with_match_locals(pattern, &block)
39
+ bindings = p(pattern).match(exp)
40
+ bindings.should_not be_nil
41
+
42
+ block.call(bindings) if block
43
+ end
44
+
45
+ # Can't use #match here, so I went to the Thesaurus.
46
+ #
47
+ RSpec::Matchers.define :detect do |pattern|
48
+ match do |tree|
49
+ bindings = Parsanol::Pattern.new(pattern).match(tree)
50
+
51
+ bindings ? true : false
52
+ end
53
+ end
54
+
55
+ describe "<- #match" do
56
+ context "injecting bindings" do
57
+ let(:pattern) { p(simple(:x)) }
58
+
59
+ it "should not modify the original bindings hash" do
60
+ h = {}
61
+ b=pattern.match('a', h)
62
+ h.size.should == 0
63
+ b.size.should == 1
64
+ end
65
+ it "should return nil when no match succeeds" do
66
+ pattern.match([], :foo => :bar).should be_nil
67
+ end
68
+ context "when matching simple(:x) against 'a'" do
69
+ let(:bindings) { pattern.match(t('a'), :foo => :bar) }
70
+
71
+ before(:each) { bindings.should_not be_nil }
72
+ it "should return the injected bindings" do
73
+ bindings[:foo].should == :bar
74
+ end
75
+ it "should return the new bindings" do
76
+ bindings[:x].should == 'a'
77
+ end
78
+ end
79
+ end
80
+ context "simple strings" do
81
+ let(:exp) { 'aaaa' }
82
+
83
+ it "should match simple strings" do
84
+ exp.should match_with_bind(simple(:x), :x => 'aaaa')
85
+ end
86
+ end
87
+ context "simple hash {:a => 'b'}" do
88
+ attr_reader :exp
89
+ before(:each) do
90
+ @exp = t(:a => 'b')
91
+ end
92
+
93
+ it "should not match {:a => simple(:x), :b => simple(:y)}" do
94
+ exp.should_not detect(:a => simple(:x), :b => simple(:y))
95
+ end
96
+ it "should match {:a => simple(:x)}, binding 'x' to the first argument" do
97
+ exp.should match_with_bind({:a => simple(:x)}, :x => 'b')
98
+ end
99
+ it "should match {:a => 'b'} with no binds" do
100
+ exp.should match_with_bind({:a => 'b'}, {})
101
+ end
102
+ end
103
+ context "a more complex hash {:a => {:b => 'c'}}" do
104
+ attr_reader :exp
105
+ before(:each) do
106
+ @exp = t(:a => {:b => 'c'})
107
+ end
108
+
109
+ it "should match wholly with {:a => {:b => simple(:x)}}" do
110
+ exp.should match_with_bind({:a => {:b => simple(:x)}}, :x => 'c')
111
+ end
112
+ it "should match wholly with {:a => subtree(:t)}" do
113
+ with_match_locals(:a => subtree(:t)) do |dict|
114
+ dict[:t].should == {:b => 'c'}
115
+ end
116
+ end
117
+ it "should not bind subtrees to variables in {:a => simple(:x)}" do
118
+ p(:a => simple(:x)).should_not detect(exp)
119
+ end
120
+ end
121
+ context "a more complex hash {:a => 'a', :b => 'b'}" do
122
+ attr_reader :exp
123
+ before(:each) do
124
+ @exp = t({:a => 'a', :b => 'b'})
125
+ end
126
+
127
+ it "should not match partially" do
128
+ Parsanol::Pattern.new(:a => simple(:x)).match(exp).should be_nil
129
+ end
130
+ it "should match completely" do
131
+ exp.should match_with_bind({:a => simple(:x), :b => simple(:y)},
132
+ :x => 'a',
133
+ :y => 'b')
134
+ end
135
+ end
136
+ context "an array of 'a', 'b', 'c'" do
137
+ let(:exp) { ['a', 'b', 'c'] }
138
+
139
+ it "should match all elements at once" do
140
+ exp.should match_with_bind(
141
+ [simple(:x), simple(:y), simple(:z)],
142
+ :x => 'a', :y => 'b', :z => 'c')
143
+ end
144
+ end
145
+ context "{:a => 'a', :b => 'b'}" do
146
+ attr_reader :exp
147
+ before(:each) do
148
+ @exp = t(:a => 'a', :b => 'b')
149
+ end
150
+
151
+ it "should match both elements simple(:x), simple(:y)" do
152
+ exp.should match_with_bind(
153
+ {:a => simple(:x), :b => simple(:y)},
154
+ :x => 'a', :y => 'b')
155
+ end
156
+ it "should not match a constrained match (simple(:x) != simple(:y))" do
157
+ exp.should_not detect({:a => simple(:x), :b => simple(:x)})
158
+ end
159
+ end
160
+ context "{:a => 'a', :b => 'a'}" do
161
+ attr_reader :exp
162
+ before(:each) do
163
+ @exp = t(:a => 'a', :b => 'a')
164
+ end
165
+
166
+ it "should match constrained pattern" do
167
+ exp.should match_with_bind(
168
+ {:a => simple(:x), :b => simple(:x)},
169
+ :x => 'a')
170
+ end
171
+ end
172
+ context "{:sub1 => {:a => 'a'}, :sub2 => {:a => 'a'}}" do
173
+ attr_reader :exp
174
+ before(:each) do
175
+ @exp = t({
176
+ :sub1 => {:a => 'a'},
177
+ :sub2 => {:a => 'a'}
178
+ })
179
+ end
180
+
181
+ it "should verify constraints over several subtrees" do
182
+ exp.should match_with_bind({
183
+ :sub1 => {:a => simple(:x)},
184
+ :sub2 => {:a => simple(:x)}
185
+ }, :x => 'a')
186
+ end
187
+ it "should return both bind variables simple(:x), simple(:y)" do
188
+ exp.should match_with_bind({
189
+ :sub1 => {:a => simple(:x)},
190
+ :sub2 => {:a => simple(:y)}
191
+ }, :x => 'a', :y => 'a')
192
+ end
193
+ end
194
+ context "{:sub1 => {:a => 'a'}, :sub2 => {:a => 'b'}}" do
195
+ attr_reader :exp
196
+ before(:each) do
197
+ @exp = t({
198
+ :sub1 => {:a => 'a'},
199
+ :sub2 => {:a => 'b'}
200
+ })
201
+ end
202
+
203
+ it "should verify constraints over several subtrees" do
204
+ exp.should_not match_with_bind({
205
+ :sub1 => {:a => simple(:x)},
206
+ :sub2 => {:a => simple(:x)}
207
+ }, :x => 'a')
208
+ end
209
+ it "should return both bind variables simple(:x), simple(:y)" do
210
+ exp.should match_with_bind({
211
+ :sub1 => {:a => simple(:x)},
212
+ :sub2 => {:a => simple(:y)}
213
+ }, :x => 'a', :y => 'b')
214
+ end
215
+ end
216
+ context "[{:a => 'x'}, {:a => 'y'}]" do
217
+ attr_reader :exp
218
+ before(:each) do
219
+ @exp = t([{:a => 'x'}, {:a => 'y'}])
220
+ end
221
+
222
+ it "should not match sequence(:x) (as a whole)" do
223
+ exp.should_not detect(sequence(:x))
224
+ end
225
+ end
226
+ context "['x', 'y', 'z']" do
227
+ attr_reader :exp
228
+ before(:each) do
229
+ @exp = t(['x', 'y', 'z'])
230
+ end
231
+
232
+ it "should match [simple(:x), simple(:y), simple(:z)]" do
233
+ with_match_locals([simple(:x), simple(:y), simple(:z)]) do |dict|
234
+ dict[:x].should == 'x'
235
+ dict[:y].should == 'y'
236
+ dict[:z].should == 'z'
237
+ end
238
+ end
239
+ it "should match %w(x y z)" do
240
+ exp.should match_with_bind(%w(x y z), { })
241
+ end
242
+ it "should not match [simple(:x), simple(:y), simple(:x)]" do
243
+ exp.should_not detect([simple(:x), simple(:y), simple(:x)])
244
+ end
245
+ it "should not match [simple(:x), simple(:y)]" do
246
+ exp.should_not detect([simple(:x), simple(:y), simple(:x)])
247
+ end
248
+ it "should match sequence(:x) (as array)" do
249
+ exp.should match_with_bind(sequence(:x), :x => ['x', 'y', 'z'])
250
+ end
251
+ end
252
+ context "{:a => [1,2,3]}" do
253
+ attr_reader :exp
254
+ before(:each) do
255
+ @exp = t(:a => [1,2,3])
256
+ end
257
+
258
+ it "should match :a => sequence(:x) (binding x to the whole array)" do
259
+ exp.should match_with_bind({:a => sequence(:x)}, {:x => [1,2,3]})
260
+ end
261
+ end
262
+ context "with differently ordered hashes" do
263
+ it "should still match" do
264
+ t(:a => 'a', :b => 'b').should detect(:a => 'a', :b => 'b')
265
+ t(:a => 'a', :b => 'b').should detect(:b => 'b', :a => 'a')
266
+
267
+ t(:b => 'b', :a => 'a').should detect(:b => 'b', :a => 'a')
268
+ t(:b => 'b', :a => 'a').should detect(:a => 'a', :b => 'b')
269
+ end
270
+ end
271
+ end
272
+ end