plurimath-parslet 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.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +284 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +454 -0
  5. data/Rakefile +71 -0
  6. data/lib/parslet/accelerator/application.rb +62 -0
  7. data/lib/parslet/accelerator/engine.rb +112 -0
  8. data/lib/parslet/accelerator.rb +162 -0
  9. data/lib/parslet/atoms/alternative.rb +53 -0
  10. data/lib/parslet/atoms/base.rb +157 -0
  11. data/lib/parslet/atoms/can_flatten.rb +137 -0
  12. data/lib/parslet/atoms/capture.rb +38 -0
  13. data/lib/parslet/atoms/context.rb +103 -0
  14. data/lib/parslet/atoms/dsl.rb +112 -0
  15. data/lib/parslet/atoms/dynamic.rb +32 -0
  16. data/lib/parslet/atoms/entity.rb +45 -0
  17. data/lib/parslet/atoms/ignored.rb +26 -0
  18. data/lib/parslet/atoms/infix.rb +115 -0
  19. data/lib/parslet/atoms/lookahead.rb +52 -0
  20. data/lib/parslet/atoms/named.rb +32 -0
  21. data/lib/parslet/atoms/re.rb +41 -0
  22. data/lib/parslet/atoms/repetition.rb +87 -0
  23. data/lib/parslet/atoms/scope.rb +26 -0
  24. data/lib/parslet/atoms/sequence.rb +48 -0
  25. data/lib/parslet/atoms/str.rb +42 -0
  26. data/lib/parslet/atoms/visitor.rb +89 -0
  27. data/lib/parslet/atoms.rb +34 -0
  28. data/lib/parslet/cause.rb +101 -0
  29. data/lib/parslet/context.rb +21 -0
  30. data/lib/parslet/convenience.rb +33 -0
  31. data/lib/parslet/error_reporter/contextual.rb +120 -0
  32. data/lib/parslet/error_reporter/deepest.rb +100 -0
  33. data/lib/parslet/error_reporter/tree.rb +63 -0
  34. data/lib/parslet/error_reporter.rb +8 -0
  35. data/lib/parslet/export.rb +163 -0
  36. data/lib/parslet/expression/treetop.rb +92 -0
  37. data/lib/parslet/expression.rb +51 -0
  38. data/lib/parslet/graphviz.rb +97 -0
  39. data/lib/parslet/parser.rb +68 -0
  40. data/lib/parslet/pattern/binding.rb +49 -0
  41. data/lib/parslet/pattern.rb +113 -0
  42. data/lib/parslet/position.rb +21 -0
  43. data/lib/parslet/rig/rspec.rb +52 -0
  44. data/lib/parslet/scope.rb +42 -0
  45. data/lib/parslet/slice.rb +105 -0
  46. data/lib/parslet/source/line_cache.rb +99 -0
  47. data/lib/parslet/source.rb +96 -0
  48. data/lib/parslet/transform.rb +265 -0
  49. data/lib/parslet/version.rb +5 -0
  50. data/lib/parslet.rb +314 -0
  51. data/plurimath-parslet.gemspec +42 -0
  52. data/spec/acceptance/infix_parser_spec.rb +145 -0
  53. data/spec/acceptance/mixing_parsers_spec.rb +74 -0
  54. data/spec/acceptance/regression_spec.rb +329 -0
  55. data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
  56. data/spec/acceptance/unconsumed_input_spec.rb +21 -0
  57. data/spec/examples/boolean_algebra_spec.rb +257 -0
  58. data/spec/examples/calc_spec.rb +278 -0
  59. data/spec/examples/capture_spec.rb +137 -0
  60. data/spec/examples/comments_spec.rb +186 -0
  61. data/spec/examples/deepest_errors_spec.rb +420 -0
  62. data/spec/examples/documentation_spec.rb +205 -0
  63. data/spec/examples/email_parser_spec.rb +275 -0
  64. data/spec/examples/empty_spec.rb +37 -0
  65. data/spec/examples/erb_spec.rb +482 -0
  66. data/spec/examples/ip_address_spec.rb +153 -0
  67. data/spec/examples/json_spec.rb +413 -0
  68. data/spec/examples/local_spec.rb +302 -0
  69. data/spec/examples/mathn_spec.rb +151 -0
  70. data/spec/examples/minilisp_spec.rb +492 -0
  71. data/spec/examples/modularity_spec.rb +340 -0
  72. data/spec/examples/nested_errors_spec.rb +322 -0
  73. data/spec/examples/optimized_erb_spec.rb +299 -0
  74. data/spec/examples/parens_spec.rb +239 -0
  75. data/spec/examples/prec_calc_spec.rb +525 -0
  76. data/spec/examples/readme_spec.rb +228 -0
  77. data/spec/examples/scopes_spec.rb +187 -0
  78. data/spec/examples/seasons_spec.rb +196 -0
  79. data/spec/examples/sentence_spec.rb +119 -0
  80. data/spec/examples/simple_xml_spec.rb +250 -0
  81. data/spec/examples/string_parser_spec.rb +407 -0
  82. data/spec/fixtures/examples/boolean_algebra.rb +62 -0
  83. data/spec/fixtures/examples/calc.rb +86 -0
  84. data/spec/fixtures/examples/capture.rb +36 -0
  85. data/spec/fixtures/examples/comments.rb +22 -0
  86. data/spec/fixtures/examples/deepest_errors.rb +99 -0
  87. data/spec/fixtures/examples/documentation.rb +32 -0
  88. data/spec/fixtures/examples/email_parser.rb +42 -0
  89. data/spec/fixtures/examples/empty.rb +10 -0
  90. data/spec/fixtures/examples/erb.rb +39 -0
  91. data/spec/fixtures/examples/ip_address.rb +103 -0
  92. data/spec/fixtures/examples/json.rb +107 -0
  93. data/spec/fixtures/examples/local.rb +60 -0
  94. data/spec/fixtures/examples/mathn.rb +47 -0
  95. data/spec/fixtures/examples/minilisp.rb +75 -0
  96. data/spec/fixtures/examples/modularity.rb +60 -0
  97. data/spec/fixtures/examples/nested_errors.rb +95 -0
  98. data/spec/fixtures/examples/optimized_erb.rb +105 -0
  99. data/spec/fixtures/examples/parens.rb +25 -0
  100. data/spec/fixtures/examples/prec_calc.rb +71 -0
  101. data/spec/fixtures/examples/readme.rb +59 -0
  102. data/spec/fixtures/examples/scopes.rb +43 -0
  103. data/spec/fixtures/examples/seasons.rb +40 -0
  104. data/spec/fixtures/examples/sentence.rb +18 -0
  105. data/spec/fixtures/examples/simple_xml.rb +51 -0
  106. data/spec/fixtures/examples/string_parser.rb +77 -0
  107. data/spec/parslet/atom_results_spec.rb +39 -0
  108. data/spec/parslet/atoms/alternative_spec.rb +26 -0
  109. data/spec/parslet/atoms/base_spec.rb +127 -0
  110. data/spec/parslet/atoms/capture_spec.rb +21 -0
  111. data/spec/parslet/atoms/combinations_spec.rb +5 -0
  112. data/spec/parslet/atoms/dsl_spec.rb +7 -0
  113. data/spec/parslet/atoms/entity_spec.rb +77 -0
  114. data/spec/parslet/atoms/ignored_spec.rb +15 -0
  115. data/spec/parslet/atoms/infix_spec.rb +5 -0
  116. data/spec/parslet/atoms/lookahead_spec.rb +22 -0
  117. data/spec/parslet/atoms/named_spec.rb +4 -0
  118. data/spec/parslet/atoms/re_spec.rb +14 -0
  119. data/spec/parslet/atoms/repetition_spec.rb +24 -0
  120. data/spec/parslet/atoms/scope_spec.rb +26 -0
  121. data/spec/parslet/atoms/sequence_spec.rb +28 -0
  122. data/spec/parslet/atoms/str_spec.rb +15 -0
  123. data/spec/parslet/atoms/visitor_spec.rb +101 -0
  124. data/spec/parslet/atoms_spec.rb +488 -0
  125. data/spec/parslet/convenience_spec.rb +54 -0
  126. data/spec/parslet/error_reporter/contextual_spec.rb +118 -0
  127. data/spec/parslet/error_reporter/deepest_spec.rb +82 -0
  128. data/spec/parslet/error_reporter/tree_spec.rb +7 -0
  129. data/spec/parslet/export_spec.rb +40 -0
  130. data/spec/parslet/expression/treetop_spec.rb +74 -0
  131. data/spec/parslet/minilisp.citrus +29 -0
  132. data/spec/parslet/minilisp.tt +29 -0
  133. data/spec/parslet/parser_spec.rb +36 -0
  134. data/spec/parslet/parslet_spec.rb +38 -0
  135. data/spec/parslet/pattern_spec.rb +272 -0
  136. data/spec/parslet/position_spec.rb +14 -0
  137. data/spec/parslet/rig/rspec_spec.rb +54 -0
  138. data/spec/parslet/scope_spec.rb +45 -0
  139. data/spec/parslet/slice_spec.rb +186 -0
  140. data/spec/parslet/source/line_cache_spec.rb +74 -0
  141. data/spec/parslet/source_spec.rb +210 -0
  142. data/spec/parslet/transform/context_spec.rb +56 -0
  143. data/spec/parslet/transform_spec.rb +183 -0
  144. data/spec/spec_helper.rb +74 -0
  145. data/spec/support/opal.rb +8 -0
  146. data/spec/support/opal.rb.erb +14 -0
  147. data/spec/support/parslet_matchers.rb +96 -0
  148. metadata +240 -0
@@ -0,0 +1,302 @@
1
+ require 'spec_helper'
2
+ require 'fixtures/examples/local'
3
+
4
+ RSpec.describe 'Local Example' do
5
+ include Parslet
6
+
7
+ describe 'LocalExample module methods' do
8
+ describe '.this' do
9
+ it 'creates a Parslet::Atoms::Entity' do
10
+ entity = LocalExample.this('test') { str('a') }
11
+ expect(entity).to be_a(Parslet::Atoms::Entity)
12
+ end
13
+
14
+ it 'accepts a block for recursive definitions' do
15
+ expect { LocalExample.this('test') { str('a') } }.not_to raise_error
16
+ end
17
+ end
18
+
19
+ describe '.epsilon' do
20
+ it 'creates an epsilon parser (matches empty string)' do
21
+ epsilon = LocalExample.epsilon
22
+ expect(epsilon).to be_a(Parslet::Atoms::Base)
23
+ end
24
+
25
+ it 'matches empty input' do
26
+ result = LocalExample.epsilon.parse('')
27
+ expect(result.to_s).to eq('')
28
+ end
29
+
30
+ it 'fails on non-empty input' do
31
+ expect { LocalExample.epsilon.parse('a') }.to raise_error(Parslet::ParseFailed)
32
+ end
33
+ end
34
+
35
+ describe '.simple_inline_parser' do
36
+ it 'parses minimum valid input ("aa")' do
37
+ result = LocalExample.simple_inline_parser.parse('aa')
38
+ expect(result.to_s).to eq('aa')
39
+ end
40
+
41
+ it 'parses sequences with additional "a"s before "aa"' do
42
+ result = LocalExample.simple_inline_parser.parse('aaa')
43
+ expect(result.to_s).to eq('aaa')
44
+ end
45
+
46
+ it 'parses longer sequences' do
47
+ result = LocalExample.simple_inline_parser.parse('aaaaa')
48
+ expect(result.to_s).to eq('aaaaa')
49
+ end
50
+
51
+ it 'parses longest supported sequence' do
52
+ result = LocalExample.simple_inline_parser.parse('aaaaaa')
53
+ expect(result.to_s).to eq('aaaaaa')
54
+ end
55
+
56
+ it 'fails on sequences longer than supported' do
57
+ expect { LocalExample.simple_inline_parser.parse('aaaaaaa') }.to raise_error(Parslet::ParseFailed)
58
+ end
59
+
60
+ it 'fails on single "a"' do
61
+ expect { LocalExample.simple_inline_parser.parse('a') }.to raise_error(Parslet::ParseFailed)
62
+ end
63
+
64
+ it 'fails on empty input' do
65
+ expect { LocalExample.simple_inline_parser.parse('') }.to raise_error(Parslet::ParseFailed)
66
+ end
67
+
68
+ it 'fails on non-"a" characters' do
69
+ expect { LocalExample.simple_inline_parser.parse('baa') }.to raise_error(Parslet::ParseFailed)
70
+ end
71
+ end
72
+
73
+ describe '.greedy_blind_parser' do
74
+ it 'parses sequences of "a" greedily' do
75
+ result = LocalExample.greedy_blind_parser.parse('aaaa')
76
+ expect(result).to be_a(Hash)
77
+ expect(result).to have_key(:e)
78
+ end
79
+
80
+ it 'handles empty input (epsilon case)' do
81
+ result = LocalExample.greedy_blind_parser.parse('')
82
+ expect(result.to_s).to eq('')
83
+ end
84
+
85
+ it 'handles single "a"' do
86
+ result = LocalExample.greedy_blind_parser.parse('a')
87
+ expect(result).to be_a(Hash)
88
+ expect(result[:e].to_s).to eq('a')
89
+ end
90
+
91
+ it 'creates recursive structure for multiple "a"s' do
92
+ result = LocalExample.greedy_blind_parser.parse('aaa')
93
+ expect(result).to be_a(Hash)
94
+ expect(result).to have_key(:e)
95
+ expect(result).to have_key(:rec)
96
+ end
97
+
98
+ it 'fails on non-"a" characters' do
99
+ expect { LocalExample.greedy_blind_parser.parse('b') }.to raise_error(Parslet::ParseFailed)
100
+ end
101
+ end
102
+
103
+ describe '.greedy_non_blind_parser' do
104
+ it 'parses "aa" as end pattern' do
105
+ result = LocalExample.greedy_non_blind_parser.parse('aa')
106
+ expect(result).to be_a(Hash)
107
+ expect(result).to have_key(:e2)
108
+ expect(result[:e2].to_s).to eq('aa')
109
+ end
110
+
111
+ it 'handles longer sequences ending with "aa"' do
112
+ result = LocalExample.greedy_non_blind_parser.parse('aaaa')
113
+ expect(result).to be_a(Hash)
114
+ # Should have both e1 and rec components
115
+ expect(result).to have_key(:e1)
116
+ expect(result).to have_key(:rec)
117
+ end
118
+
119
+ it 'creates recursive structure for complex patterns' do
120
+ result = LocalExample.greedy_non_blind_parser.parse('aaaaaa')
121
+ expect(result).to be_a(Hash)
122
+ expect(result).to have_key(:e1)
123
+ expect(result).to have_key(:rec)
124
+ end
125
+
126
+ it 'fails on single "a"' do
127
+ expect { LocalExample.greedy_non_blind_parser.parse('a') }.to raise_error(Parslet::ParseFailed)
128
+ end
129
+
130
+ it 'fails on empty input' do
131
+ expect { LocalExample.greedy_non_blind_parser.parse('') }.to raise_error(Parslet::ParseFailed)
132
+ end
133
+ end
134
+
135
+ describe '.demonstrate_local_variables' do
136
+ it 'returns a parser constructed with local variables' do
137
+ parser = LocalExample.demonstrate_local_variables
138
+ expect(parser).to be_a(Parslet::Atoms::Base)
139
+ end
140
+
141
+ it 'creates a working parser from local variables' do
142
+ parser = LocalExample.demonstrate_local_variables
143
+ result = parser.parse('aaaa')
144
+ expect(result.to_s).to eq('aaaa')
145
+ end
146
+
147
+ it 'demonstrates local variable scoping' do
148
+ parser = LocalExample.demonstrate_local_variables
149
+ expect { parser.parse('aa') }.not_to raise_error
150
+ end
151
+ end
152
+ end
153
+
154
+ describe 'parsing methods' do
155
+ describe '.parse_with_greedy_blind' do
156
+ it 'parses input using greedy blind parser' do
157
+ result = LocalExample.parse_with_greedy_blind('aaa')
158
+ expect(result).to be_a(Hash)
159
+ expect(result).to have_key(:e)
160
+ end
161
+
162
+ it 'handles empty input' do
163
+ result = LocalExample.parse_with_greedy_blind('')
164
+ expect(result.to_s).to eq('')
165
+ end
166
+
167
+ it 'raises error for invalid input' do
168
+ expect { LocalExample.parse_with_greedy_blind('b') }.to raise_error(Parslet::ParseFailed)
169
+ end
170
+ end
171
+
172
+ describe '.parse_with_greedy_non_blind' do
173
+ it 'parses input using greedy non-blind parser' do
174
+ result = LocalExample.parse_with_greedy_non_blind('aaaa')
175
+ expect(result).to be_a(Hash)
176
+ end
177
+
178
+ it 'handles minimum valid input' do
179
+ result = LocalExample.parse_with_greedy_non_blind('aa')
180
+ expect(result).to be_a(Hash)
181
+ expect(result[:e2].to_s).to eq('aa')
182
+ end
183
+
184
+ it 'raises error for insufficient input' do
185
+ expect { LocalExample.parse_with_greedy_non_blind('a') }.to raise_error(Parslet::ParseFailed)
186
+ end
187
+ end
188
+
189
+ describe '.parse_with_simple_inline' do
190
+ it 'parses input using simple inline parser (minimum: "aa")' do
191
+ result = LocalExample.parse_with_simple_inline('aa')
192
+ expect(result.to_s).to eq('aa')
193
+ end
194
+
195
+ it 'parses longer sequences' do
196
+ result = LocalExample.parse_with_simple_inline('aaaaa')
197
+ expect(result.to_s).to eq('aaaaa')
198
+ end
199
+
200
+ it 'raises error for single "a"' do
201
+ expect { LocalExample.parse_with_simple_inline('a') }.to raise_error(Parslet::ParseFailed)
202
+ end
203
+
204
+ it 'raises error for empty input' do
205
+ expect { LocalExample.parse_with_simple_inline('') }.to raise_error(Parslet::ParseFailed)
206
+ end
207
+ end
208
+ end
209
+
210
+ describe 'advanced parsing concepts' do
211
+ describe 'inline parser construction' do
212
+ it 'demonstrates parser construction without class wrapper' do
213
+ # This shows the concept of building parsers inline
214
+ inline_parser = str('hello') >> str(' ') >> str('world')
215
+ result = inline_parser.parse('hello world')
216
+ expect(result.to_s).to eq('hello world')
217
+ end
218
+
219
+ it 'shows local variable usage in parser building' do
220
+ # Demonstrates using local variables to build parsers
221
+ greeting = str('hello')
222
+ space = str(' ')
223
+ target = str('world')
224
+ parser = greeting >> space >> target
225
+
226
+ result = parser.parse('hello world')
227
+ expect(result.to_s).to eq('hello world')
228
+ end
229
+ end
230
+
231
+ describe 'greedy vs non-greedy parsing' do
232
+ it 'compares greedy blind vs greedy non-blind behavior' do
233
+ input = 'aaaa'
234
+
235
+ # Both should parse successfully but with different structures
236
+ blind_result = LocalExample.parse_with_greedy_blind(input)
237
+ non_blind_result = LocalExample.parse_with_greedy_non_blind(input)
238
+
239
+ expect(blind_result).to be_a(Hash)
240
+ expect(non_blind_result).to be_a(Hash)
241
+
242
+ # They should have different structures
243
+ expect(blind_result.keys).not_to eq(non_blind_result.keys)
244
+ end
245
+
246
+ it 'shows different parsing strategies for same input' do
247
+ input = 'aaaaaa'
248
+
249
+ # Test that both parsers handle the input but differently
250
+ expect { LocalExample.parse_with_greedy_blind(input) }.not_to raise_error
251
+ expect { LocalExample.parse_with_greedy_non_blind(input) }.not_to raise_error
252
+
253
+ blind_result = LocalExample.parse_with_greedy_blind(input)
254
+ non_blind_result = LocalExample.parse_with_greedy_non_blind(input)
255
+
256
+ # Results should be different in structure
257
+ expect(blind_result).not_to eq(non_blind_result)
258
+ end
259
+ end
260
+
261
+ describe 'recursive parser construction' do
262
+ it 'demonstrates recursive entity usage' do
263
+ # The greedy parsers use recursive entities
264
+ result = LocalExample.parse_with_greedy_blind('aaaaa')
265
+
266
+ # Should create nested recursive structure
267
+ expect(result).to have_key(:e)
268
+ expect(result).to have_key(:rec)
269
+
270
+ # Recursive structure should be present
271
+ current = result
272
+ depth = 0
273
+ while current.is_a?(Hash) && current.key?(:rec) && current[:rec].is_a?(Hash)
274
+ current = current[:rec]
275
+ depth += 1
276
+ break if depth > 10 # Safety check
277
+ end
278
+
279
+ expect(depth).to be > 0
280
+ end
281
+ end
282
+
283
+ describe 'epsilon parser behavior' do
284
+ it 'demonstrates epsilon as empty match' do
285
+ epsilon = LocalExample.epsilon
286
+
287
+ # Should match empty string
288
+ expect { epsilon.parse('') }.not_to raise_error
289
+
290
+ # Should fail on any content
291
+ expect { epsilon.parse('a') }.to raise_error(Parslet::ParseFailed)
292
+ expect { epsilon.parse(' ') }.to raise_error(Parslet::ParseFailed)
293
+ end
294
+
295
+ it 'shows epsilon usage in alternatives' do
296
+ # Epsilon is used in the greedy blind parser as an alternative
297
+ result = LocalExample.parse_with_greedy_blind('')
298
+ expect(result.to_s).to eq('')
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,151 @@
1
+ require 'spec_helper'
2
+
3
+ # Load the example file to get the classes
4
+ $:.unshift File.dirname(__FILE__) + "/../../example"
5
+
6
+ RSpec.describe 'Mathn Compatibility Example' do
7
+ describe 'parser behavior' do
8
+ let(:possible_whitespace) { Parslet.match['\s'].repeat }
9
+ let(:cephalopod) { Parslet.str('octopus') | Parslet.str('squid') }
10
+ let(:parenthesized_cephalopod) do
11
+ Parslet.str('(') >>
12
+ possible_whitespace >>
13
+ cephalopod >>
14
+ possible_whitespace >>
15
+ Parslet.str(')')
16
+ end
17
+ let(:parser) do
18
+ possible_whitespace >>
19
+ parenthesized_cephalopod >>
20
+ possible_whitespace
21
+ end
22
+
23
+ it 'fails to parse invalid input as expected' do
24
+ # This should fail to parse because "sqeed" is not "squid" or "octopus"
25
+ expect { parser.parse %{(\nsqeed)\n} }.to raise_error(Parslet::ParseFailed)
26
+ end
27
+
28
+ it 'successfully parses valid cephalopod input' do
29
+ result = parser.parse %{(
30
+ squid)
31
+ }
32
+ expect(result.to_s).to eq("(\nsquid)\n")
33
+
34
+ result = parser.parse %{( octopus )}
35
+ expect(result.to_s).to eq('( octopus )')
36
+ end
37
+
38
+ it 'handles whitespace correctly' do
39
+ result = parser.parse %{(squid)}
40
+ expect(result.to_s).to eq('(squid)')
41
+
42
+ result = parser.parse %{ ( squid ) }
43
+ expect(result.to_s).to eq(' ( squid ) ')
44
+ end
45
+ end
46
+
47
+ describe 'mathn compatibility' do
48
+ def attempt_parse_with_timeout(timeout = 5)
49
+ # Opal doesn't support Threads, so we'll just run the parse directly
50
+ # The mathn compatibility issue was specific to MRI Ruby anyway
51
+ begin
52
+ possible_whitespace = Parslet.match['\s'].repeat
53
+ cephalopod = Parslet.str('octopus') | Parslet.str('squid')
54
+ parenthesized_cephalopod = Parslet.str('(') >> possible_whitespace >> cephalopod >> possible_whitespace >> Parslet.str(')')
55
+ parser = possible_whitespace >> parenthesized_cephalopod >> possible_whitespace
56
+
57
+ parser.parse %{(\nsqeed)\n}
58
+ rescue Parslet::ParseFailed => e
59
+ raise e
60
+ rescue => e
61
+ raise e
62
+ end
63
+ end
64
+
65
+ it 'terminates properly before requiring mathn' do
66
+ # This should fail with ParseFailed, not hang
67
+ # In Opal, we don't have the mathn compatibility issue, so this should work fine
68
+ expect { attempt_parse_with_timeout }.to raise_error(Parslet::ParseFailed)
69
+ end
70
+
71
+ # Skip mathn test on Ruby 2.5+ since mathn was deprecated
72
+ if RUBY_VERSION.gsub(/[^\d]/, '').to_i < 250
73
+ it 'still terminates properly after requiring mathn' do
74
+ # Require mathn in an isolated way
75
+ begin
76
+ require 'mathn'
77
+
78
+ # This should still fail with ParseFailed, not hang
79
+ # The fix in parslet should prevent infinite loops even with mathn loaded
80
+ expect { attempt_parse_with_timeout }.to raise_error(Parslet::ParseFailed)
81
+ rescue LoadError
82
+ skip "mathn not available in this Ruby version"
83
+ end
84
+ end
85
+ else
86
+ it 'skips mathn test on Ruby 2.5+' do
87
+ skip "mathn was deprecated in Ruby 2.5+"
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'integration test' do
93
+ it 'demonstrates the mathn compatibility fix' do
94
+ # The key point of this example is that parslet should not hang
95
+ # when mathn is loaded, even with failing parses
96
+
97
+ # Test that we can create the parser components
98
+ possible_whitespace = Parslet.match['\s'].repeat
99
+ expect(possible_whitespace).to be_a(Parslet::Atoms::Repetition)
100
+
101
+ cephalopod = Parslet.str('octopus') | Parslet.str('squid')
102
+ expect(cephalopod).to be_a(Parslet::Atoms::Alternative)
103
+
104
+ parenthesized_cephalopod = Parslet.str('(') >> possible_whitespace >> cephalopod >> possible_whitespace >> Parslet.str(')')
105
+ expect(parenthesized_cephalopod).to be_a(Parslet::Atoms::Sequence)
106
+
107
+ parser = possible_whitespace >> parenthesized_cephalopod >> possible_whitespace
108
+ expect(parser).to be_a(Parslet::Atoms::Sequence)
109
+
110
+ # Test that parsing fails appropriately (not hangs)
111
+ expect { parser.parse %{(\nsqeed)\n} }.to raise_error(Parslet::ParseFailed)
112
+ end
113
+
114
+ it 'reproduces the example behavior' do
115
+ # This test reproduces what the example file does:
116
+ # 1. Attempts to parse invalid input (should fail)
117
+ # 2. Shows that it terminates properly
118
+
119
+ output = capture_output do
120
+ # Simulate the attempt_parse function
121
+ begin
122
+ possible_whitespace = Parslet.match['\s'].repeat
123
+ cephalopod = Parslet.str('octopus') | Parslet.str('squid')
124
+ parenthesized_cephalopod = Parslet.str('(') >> possible_whitespace >> cephalopod >> possible_whitespace >> Parslet.str(')')
125
+ parser = possible_whitespace >> parenthesized_cephalopod >> possible_whitespace
126
+
127
+ parser.parse %{(\nsqeed)\n}
128
+ rescue Parslet::ParseFailed
129
+ # Expected - this is what should happen
130
+ end
131
+
132
+ puts 'it terminates before we require mathn'
133
+ puts 'okay!'
134
+ end
135
+
136
+ expect(output).to include('it terminates before we require mathn')
137
+ expect(output).to include('okay!')
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def capture_output
144
+ original_stdout = $stdout
145
+ $stdout = StringIO.new
146
+ yield
147
+ $stdout.string
148
+ ensure
149
+ $stdout = original_stdout
150
+ end
151
+ end