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.
- checksums.yaml +7 -0
- data/HISTORY.txt +284 -0
- data/LICENSE +23 -0
- data/README.adoc +454 -0
- data/Rakefile +71 -0
- data/lib/parslet/accelerator/application.rb +62 -0
- data/lib/parslet/accelerator/engine.rb +112 -0
- data/lib/parslet/accelerator.rb +162 -0
- data/lib/parslet/atoms/alternative.rb +53 -0
- data/lib/parslet/atoms/base.rb +157 -0
- data/lib/parslet/atoms/can_flatten.rb +137 -0
- data/lib/parslet/atoms/capture.rb +38 -0
- data/lib/parslet/atoms/context.rb +103 -0
- data/lib/parslet/atoms/dsl.rb +112 -0
- data/lib/parslet/atoms/dynamic.rb +32 -0
- data/lib/parslet/atoms/entity.rb +45 -0
- data/lib/parslet/atoms/ignored.rb +26 -0
- data/lib/parslet/atoms/infix.rb +115 -0
- data/lib/parslet/atoms/lookahead.rb +52 -0
- data/lib/parslet/atoms/named.rb +32 -0
- data/lib/parslet/atoms/re.rb +41 -0
- data/lib/parslet/atoms/repetition.rb +87 -0
- data/lib/parslet/atoms/scope.rb +26 -0
- data/lib/parslet/atoms/sequence.rb +48 -0
- data/lib/parslet/atoms/str.rb +42 -0
- data/lib/parslet/atoms/visitor.rb +89 -0
- data/lib/parslet/atoms.rb +34 -0
- data/lib/parslet/cause.rb +101 -0
- data/lib/parslet/context.rb +21 -0
- data/lib/parslet/convenience.rb +33 -0
- data/lib/parslet/error_reporter/contextual.rb +120 -0
- data/lib/parslet/error_reporter/deepest.rb +100 -0
- data/lib/parslet/error_reporter/tree.rb +63 -0
- data/lib/parslet/error_reporter.rb +8 -0
- data/lib/parslet/export.rb +163 -0
- data/lib/parslet/expression/treetop.rb +92 -0
- data/lib/parslet/expression.rb +51 -0
- data/lib/parslet/graphviz.rb +97 -0
- data/lib/parslet/parser.rb +68 -0
- data/lib/parslet/pattern/binding.rb +49 -0
- data/lib/parslet/pattern.rb +113 -0
- data/lib/parslet/position.rb +21 -0
- data/lib/parslet/rig/rspec.rb +52 -0
- data/lib/parslet/scope.rb +42 -0
- data/lib/parslet/slice.rb +105 -0
- data/lib/parslet/source/line_cache.rb +99 -0
- data/lib/parslet/source.rb +96 -0
- data/lib/parslet/transform.rb +265 -0
- data/lib/parslet/version.rb +5 -0
- data/lib/parslet.rb +314 -0
- data/plurimath-parslet.gemspec +42 -0
- data/spec/acceptance/infix_parser_spec.rb +145 -0
- data/spec/acceptance/mixing_parsers_spec.rb +74 -0
- data/spec/acceptance/regression_spec.rb +329 -0
- data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
- data/spec/acceptance/unconsumed_input_spec.rb +21 -0
- data/spec/examples/boolean_algebra_spec.rb +257 -0
- data/spec/examples/calc_spec.rb +278 -0
- data/spec/examples/capture_spec.rb +137 -0
- data/spec/examples/comments_spec.rb +186 -0
- data/spec/examples/deepest_errors_spec.rb +420 -0
- data/spec/examples/documentation_spec.rb +205 -0
- data/spec/examples/email_parser_spec.rb +275 -0
- data/spec/examples/empty_spec.rb +37 -0
- data/spec/examples/erb_spec.rb +482 -0
- data/spec/examples/ip_address_spec.rb +153 -0
- data/spec/examples/json_spec.rb +413 -0
- data/spec/examples/local_spec.rb +302 -0
- data/spec/examples/mathn_spec.rb +151 -0
- data/spec/examples/minilisp_spec.rb +492 -0
- data/spec/examples/modularity_spec.rb +340 -0
- data/spec/examples/nested_errors_spec.rb +322 -0
- data/spec/examples/optimized_erb_spec.rb +299 -0
- data/spec/examples/parens_spec.rb +239 -0
- data/spec/examples/prec_calc_spec.rb +525 -0
- data/spec/examples/readme_spec.rb +228 -0
- data/spec/examples/scopes_spec.rb +187 -0
- data/spec/examples/seasons_spec.rb +196 -0
- data/spec/examples/sentence_spec.rb +119 -0
- data/spec/examples/simple_xml_spec.rb +250 -0
- data/spec/examples/string_parser_spec.rb +407 -0
- data/spec/fixtures/examples/boolean_algebra.rb +62 -0
- data/spec/fixtures/examples/calc.rb +86 -0
- data/spec/fixtures/examples/capture.rb +36 -0
- data/spec/fixtures/examples/comments.rb +22 -0
- data/spec/fixtures/examples/deepest_errors.rb +99 -0
- data/spec/fixtures/examples/documentation.rb +32 -0
- data/spec/fixtures/examples/email_parser.rb +42 -0
- data/spec/fixtures/examples/empty.rb +10 -0
- data/spec/fixtures/examples/erb.rb +39 -0
- data/spec/fixtures/examples/ip_address.rb +103 -0
- data/spec/fixtures/examples/json.rb +107 -0
- data/spec/fixtures/examples/local.rb +60 -0
- data/spec/fixtures/examples/mathn.rb +47 -0
- data/spec/fixtures/examples/minilisp.rb +75 -0
- data/spec/fixtures/examples/modularity.rb +60 -0
- data/spec/fixtures/examples/nested_errors.rb +95 -0
- data/spec/fixtures/examples/optimized_erb.rb +105 -0
- data/spec/fixtures/examples/parens.rb +25 -0
- data/spec/fixtures/examples/prec_calc.rb +71 -0
- data/spec/fixtures/examples/readme.rb +59 -0
- data/spec/fixtures/examples/scopes.rb +43 -0
- data/spec/fixtures/examples/seasons.rb +40 -0
- data/spec/fixtures/examples/sentence.rb +18 -0
- data/spec/fixtures/examples/simple_xml.rb +51 -0
- data/spec/fixtures/examples/string_parser.rb +77 -0
- data/spec/parslet/atom_results_spec.rb +39 -0
- data/spec/parslet/atoms/alternative_spec.rb +26 -0
- data/spec/parslet/atoms/base_spec.rb +127 -0
- data/spec/parslet/atoms/capture_spec.rb +21 -0
- data/spec/parslet/atoms/combinations_spec.rb +5 -0
- data/spec/parslet/atoms/dsl_spec.rb +7 -0
- data/spec/parslet/atoms/entity_spec.rb +77 -0
- data/spec/parslet/atoms/ignored_spec.rb +15 -0
- data/spec/parslet/atoms/infix_spec.rb +5 -0
- data/spec/parslet/atoms/lookahead_spec.rb +22 -0
- data/spec/parslet/atoms/named_spec.rb +4 -0
- data/spec/parslet/atoms/re_spec.rb +14 -0
- data/spec/parslet/atoms/repetition_spec.rb +24 -0
- data/spec/parslet/atoms/scope_spec.rb +26 -0
- data/spec/parslet/atoms/sequence_spec.rb +28 -0
- data/spec/parslet/atoms/str_spec.rb +15 -0
- data/spec/parslet/atoms/visitor_spec.rb +101 -0
- data/spec/parslet/atoms_spec.rb +488 -0
- data/spec/parslet/convenience_spec.rb +54 -0
- data/spec/parslet/error_reporter/contextual_spec.rb +118 -0
- data/spec/parslet/error_reporter/deepest_spec.rb +82 -0
- data/spec/parslet/error_reporter/tree_spec.rb +7 -0
- data/spec/parslet/export_spec.rb +40 -0
- data/spec/parslet/expression/treetop_spec.rb +74 -0
- data/spec/parslet/minilisp.citrus +29 -0
- data/spec/parslet/minilisp.tt +29 -0
- data/spec/parslet/parser_spec.rb +36 -0
- data/spec/parslet/parslet_spec.rb +38 -0
- data/spec/parslet/pattern_spec.rb +272 -0
- data/spec/parslet/position_spec.rb +14 -0
- data/spec/parslet/rig/rspec_spec.rb +54 -0
- data/spec/parslet/scope_spec.rb +45 -0
- data/spec/parslet/slice_spec.rb +186 -0
- data/spec/parslet/source/line_cache_spec.rb +74 -0
- data/spec/parslet/source_spec.rb +210 -0
- data/spec/parslet/transform/context_spec.rb +56 -0
- data/spec/parslet/transform_spec.rb +183 -0
- data/spec/spec_helper.rb +74 -0
- data/spec/support/opal.rb +8 -0
- data/spec/support/opal.rb.erb +14 -0
- data/spec/support/parslet_matchers.rb +96 -0
- metadata +240 -0
@@ -0,0 +1,407 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Load the example file to get the classes
|
4
|
+
$:.unshift File.dirname(__FILE__) + "/../../example"
|
5
|
+
|
6
|
+
# Define the classes directly to avoid eval issues
|
7
|
+
require 'pp'
|
8
|
+
require 'parslet'
|
9
|
+
|
10
|
+
include Parslet
|
11
|
+
|
12
|
+
class LiteralsParser < Parslet::Parser
|
13
|
+
rule :space do
|
14
|
+
(match '[ ]').repeat(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
rule :literals do
|
18
|
+
(literal >> eol).repeat
|
19
|
+
end
|
20
|
+
|
21
|
+
rule :literal do
|
22
|
+
(integer | string).as(:literal) >> space.maybe
|
23
|
+
end
|
24
|
+
|
25
|
+
rule :string do
|
26
|
+
str('"') >>
|
27
|
+
(
|
28
|
+
(str('\\') >> any) |
|
29
|
+
(str('"').absent? >> any)
|
30
|
+
).repeat.as(:string) >>
|
31
|
+
str('"')
|
32
|
+
end
|
33
|
+
|
34
|
+
rule :integer do
|
35
|
+
match('[0-9]').repeat(1).as(:integer)
|
36
|
+
end
|
37
|
+
|
38
|
+
rule :eol do
|
39
|
+
line_end.repeat(1)
|
40
|
+
end
|
41
|
+
|
42
|
+
rule :line_end do
|
43
|
+
crlf >> space.maybe
|
44
|
+
end
|
45
|
+
|
46
|
+
rule :crlf do
|
47
|
+
match('[\r\n]').repeat(1)
|
48
|
+
end
|
49
|
+
|
50
|
+
root :literals
|
51
|
+
end
|
52
|
+
|
53
|
+
class StringParserLit < Struct.new(:text)
|
54
|
+
def to_s
|
55
|
+
text.inspect
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class StringParserStringLit < StringParserLit
|
60
|
+
end
|
61
|
+
|
62
|
+
class StringParserIntLit < StringParserLit
|
63
|
+
def to_s
|
64
|
+
text
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
RSpec.describe 'String Parser Example' do
|
69
|
+
let(:parser) { LiteralsParser.new }
|
70
|
+
let(:transform) {
|
71
|
+
Parslet::Transform.new do
|
72
|
+
rule(:literal => {:integer => simple(:x)}) { StringParserIntLit.new(x) }
|
73
|
+
rule(:literal => {:string => simple(:s)}) { StringParserStringLit.new(s) }
|
74
|
+
end
|
75
|
+
}
|
76
|
+
|
77
|
+
describe LiteralsParser do
|
78
|
+
describe '#integer' do
|
79
|
+
it 'parses single digit integers' do
|
80
|
+
result = parser.integer.parse('5')
|
81
|
+
expect(result).to eq({:integer => '5'})
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'parses multi-digit integers' do
|
85
|
+
result = parser.integer.parse('12345')
|
86
|
+
expect(result).to eq({:integer => '12345'})
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'fails on non-numeric input' do
|
90
|
+
expect { parser.integer.parse('abc') }.to raise_error(Parslet::ParseFailed)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'fails on empty input' do
|
94
|
+
expect { parser.integer.parse('') }.to raise_error(Parslet::ParseFailed)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#string' do
|
99
|
+
it 'parses simple quoted strings' do
|
100
|
+
result = parser.string.parse('"hello"')
|
101
|
+
expect(result).to eq({:string => 'hello'})
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'parses empty strings' do
|
105
|
+
result = parser.string.parse('""')
|
106
|
+
expect(result).to eq({:string => []})
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'parses strings with escaped quotes' do
|
110
|
+
result = parser.string.parse('"hello \"world\""')
|
111
|
+
expect(result).to eq({:string => 'hello \"world\"'})
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'parses strings with other escaped characters' do
|
115
|
+
result = parser.string.parse('"hello\\nworld"')
|
116
|
+
expect(result).to eq({:string => 'hello\\nworld'})
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'fails on unquoted strings' do
|
120
|
+
expect { parser.string.parse('hello') }.to raise_error(Parslet::ParseFailed)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'fails on unclosed strings' do
|
124
|
+
expect { parser.string.parse('"hello') }.to raise_error(Parslet::ParseFailed)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe '#literal' do
|
129
|
+
it 'parses integer literals' do
|
130
|
+
result = parser.literal.parse('123')
|
131
|
+
expect(result).to eq({:literal => {:integer => '123'}})
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'parses string literals' do
|
135
|
+
result = parser.literal.parse('"hello"')
|
136
|
+
expect(result).to eq({:literal => {:string => 'hello'}})
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'parses literals with trailing spaces' do
|
140
|
+
result = parser.literal.parse('123 ')
|
141
|
+
expect(result).to eq({:literal => {:integer => '123'}})
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe '#space' do
|
146
|
+
it 'parses single space' do
|
147
|
+
result = parser.space.parse(' ')
|
148
|
+
expect(result.to_s).to eq(' ')
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'parses multiple spaces' do
|
152
|
+
result = parser.space.parse(' ')
|
153
|
+
expect(result.to_s).to eq(' ')
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'fails on empty input' do
|
157
|
+
expect { parser.space.parse('') }.to raise_error(Parslet::ParseFailed)
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'fails on non-space characters' do
|
161
|
+
expect { parser.space.parse('a') }.to raise_error(Parslet::ParseFailed)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe '#crlf' do
|
166
|
+
it 'parses carriage return' do
|
167
|
+
result = parser.crlf.parse("\r")
|
168
|
+
expect(result.to_s).to eq("\r")
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'parses line feed' do
|
172
|
+
result = parser.crlf.parse("\n")
|
173
|
+
expect(result.to_s).to eq("\n")
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'parses CRLF sequence' do
|
177
|
+
result = parser.crlf.parse("\r\n")
|
178
|
+
expect(result.to_s).to eq("\r\n")
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'parses multiple newlines' do
|
182
|
+
result = parser.crlf.parse("\n\n")
|
183
|
+
expect(result.to_s).to eq("\n\n")
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe '#line_end' do
|
188
|
+
it 'parses newline without space' do
|
189
|
+
result = parser.line_end.parse("\n")
|
190
|
+
expect(result.to_s).to eq("\n")
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'parses newline with trailing space' do
|
194
|
+
result = parser.line_end.parse("\n ")
|
195
|
+
expect(result.to_s).to eq("\n ")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe '#eol' do
|
200
|
+
it 'parses single end of line' do
|
201
|
+
result = parser.eol.parse("\n")
|
202
|
+
expect(result.to_s).to eq("\n")
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'parses multiple end of lines' do
|
206
|
+
result = parser.eol.parse("\n\n")
|
207
|
+
expect(result.to_s).to eq("\n\n")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe '#literals (root)' do
|
212
|
+
it 'parses single integer literal' do
|
213
|
+
result = parser.parse("123\n")
|
214
|
+
expect(result).to eq([{:literal => {:integer => '123'}}])
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'parses single string literal' do
|
218
|
+
result = parser.parse("\"hello\"\n")
|
219
|
+
expect(result).to eq([{:literal => {:string => 'hello'}}])
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'parses multiple literals' do
|
223
|
+
input = "123\n\"hello\"\n456\n"
|
224
|
+
result = parser.parse(input)
|
225
|
+
expect(result).to eq([
|
226
|
+
{:literal => {:integer => '123'}},
|
227
|
+
{:literal => {:string => 'hello'}},
|
228
|
+
{:literal => {:integer => '456'}}
|
229
|
+
])
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'parses literals with spaces' do
|
233
|
+
input = "123 \n\"hello\" \n"
|
234
|
+
result = parser.parse(input)
|
235
|
+
expect(result).to eq([
|
236
|
+
{:literal => {:integer => '123'}},
|
237
|
+
{:literal => {:string => 'hello'}}
|
238
|
+
])
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'handles empty input' do
|
242
|
+
result = parser.parse("")
|
243
|
+
expect(result).to eq("")
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe 'Transform classes' do
|
249
|
+
describe StringParserLit do
|
250
|
+
it 'stores text and converts to inspect format' do
|
251
|
+
lit = StringParserLit.new('hello')
|
252
|
+
expect(lit.text).to eq('hello')
|
253
|
+
expect(lit.to_s).to eq('"hello"')
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe StringParserStringLit do
|
258
|
+
it 'inherits from StringParserLit' do
|
259
|
+
string_lit = StringParserStringLit.new('hello')
|
260
|
+
expect(string_lit).to be_a(StringParserLit)
|
261
|
+
expect(string_lit.text).to eq('hello')
|
262
|
+
expect(string_lit.to_s).to eq('"hello"')
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
describe StringParserIntLit do
|
267
|
+
it 'inherits from StringParserLit but shows text directly' do
|
268
|
+
int_lit = StringParserIntLit.new('123')
|
269
|
+
expect(int_lit).to be_a(StringParserLit)
|
270
|
+
expect(int_lit.text).to eq('123')
|
271
|
+
expect(int_lit.to_s).to eq('123')
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
describe 'Transform rules' do
|
277
|
+
it 'transforms integer literals to StringParserIntLit objects' do
|
278
|
+
parse_result = {:literal => {:integer => '123'}}
|
279
|
+
result = transform.apply(parse_result)
|
280
|
+
expect(result).to be_a(StringParserIntLit)
|
281
|
+
expect(result.text).to eq('123')
|
282
|
+
expect(result.to_s).to eq('123')
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'transforms string literals to StringParserStringLit objects' do
|
286
|
+
parse_result = {:literal => {:string => 'hello'}}
|
287
|
+
result = transform.apply(parse_result)
|
288
|
+
expect(result).to be_a(StringParserStringLit)
|
289
|
+
expect(result.text).to eq('hello')
|
290
|
+
expect(result.to_s).to eq('"hello"')
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'transforms arrays of literals' do
|
294
|
+
parse_result = [
|
295
|
+
{:literal => {:integer => '123'}},
|
296
|
+
{:literal => {:string => 'hello'}}
|
297
|
+
]
|
298
|
+
result = transform.apply(parse_result)
|
299
|
+
expect(result).to be_an(Array)
|
300
|
+
expect(result.length).to eq(2)
|
301
|
+
expect(result[0]).to be_a(StringParserIntLit)
|
302
|
+
expect(result[1]).to be_a(StringParserStringLit)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
describe 'integration test with simple.lit file' do
|
307
|
+
let(:simple_lit_content) { "123\n12345\n\" Some String with \\\"escapes\\\"\"\n" }
|
308
|
+
|
309
|
+
it 'parses the simple.lit file correctly' do
|
310
|
+
result = parser.parse(simple_lit_content)
|
311
|
+
expect(result).to eq([
|
312
|
+
{:literal => {:integer => '123'}},
|
313
|
+
{:literal => {:integer => '12345'}},
|
314
|
+
{:literal => {:string => ' Some String with \"escapes\"'}}
|
315
|
+
])
|
316
|
+
end
|
317
|
+
|
318
|
+
it 'transforms the simple.lit file to AST objects' do
|
319
|
+
parse_result = parser.parse(simple_lit_content)
|
320
|
+
ast = transform.apply(parse_result)
|
321
|
+
|
322
|
+
expect(ast).to be_an(Array)
|
323
|
+
expect(ast.length).to eq(3)
|
324
|
+
|
325
|
+
# First integer
|
326
|
+
expect(ast[0]).to be_a(StringParserIntLit)
|
327
|
+
expect(ast[0].text).to eq('123')
|
328
|
+
expect(ast[0].to_s).to eq('123')
|
329
|
+
|
330
|
+
# Second integer
|
331
|
+
expect(ast[1]).to be_a(StringParserIntLit)
|
332
|
+
expect(ast[1].text).to eq('12345')
|
333
|
+
expect(ast[1].to_s).to eq('12345')
|
334
|
+
|
335
|
+
# String with escapes
|
336
|
+
expect(ast[2]).to be_a(StringParserStringLit)
|
337
|
+
expect(ast[2].text.to_s).to include('Some String with')
|
338
|
+
expect(ast[2].text.to_s).to include('escapes')
|
339
|
+
expect(ast[2].to_s).to include('Some String with')
|
340
|
+
expect(ast[2].to_s).to include('escapes')
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'reproduces the example behavior' do
|
344
|
+
# This test reproduces what the example file does:
|
345
|
+
# 1. Parse the simple.lit file
|
346
|
+
# 2. Transform to AST objects
|
347
|
+
# 3. Verify the structure
|
348
|
+
|
349
|
+
parsetree = LiteralsParser.new.parse(simple_lit_content)
|
350
|
+
|
351
|
+
transform = Parslet::Transform.new do
|
352
|
+
rule(:literal => {:integer => simple(:x)}) { StringParserIntLit.new(x) }
|
353
|
+
rule(:literal => {:string => simple(:s)}) { StringParserStringLit.new(s) }
|
354
|
+
end
|
355
|
+
|
356
|
+
ast = transform.apply(parsetree)
|
357
|
+
|
358
|
+
# Verify we have the expected structure
|
359
|
+
expect(ast).to be_an(Array)
|
360
|
+
expect(ast.length).to eq(3)
|
361
|
+
expect(ast.all? { |item| item.is_a?(StringParserLit) }).to be true
|
362
|
+
|
363
|
+
# Verify the types and values
|
364
|
+
expect(ast[0]).to be_a(StringParserIntLit)
|
365
|
+
expect(ast[1]).to be_a(StringParserIntLit)
|
366
|
+
expect(ast[2]).to be_a(StringParserStringLit)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
describe 'edge cases and error handling' do
|
371
|
+
it 'handles mixed content correctly' do
|
372
|
+
input = "42\n\"test\"\n999\n\"another\"\n"
|
373
|
+
result = parser.parse(input)
|
374
|
+
expect(result.length).to eq(4)
|
375
|
+
expect(result[0][:literal][:integer]).to eq('42')
|
376
|
+
expect(result[1][:literal][:string]).to eq('test')
|
377
|
+
expect(result[2][:literal][:integer]).to eq('999')
|
378
|
+
expect(result[3][:literal][:string]).to eq('another')
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'handles strings with various escape sequences' do
|
382
|
+
input = "\"line1\\nline2\"\n\"quote: \\\"hello\\\"\"\n"
|
383
|
+
result = parser.parse(input)
|
384
|
+
expect(result.length).to eq(2)
|
385
|
+
expect(result[0][:literal][:string]).to eq('line1\\nline2')
|
386
|
+
expect(result[1][:literal][:string].to_s).to match(/quote: \\"hello\\"/)
|
387
|
+
end
|
388
|
+
|
389
|
+
it 'fails on malformed input' do
|
390
|
+
expect { parser.parse('123 "unclosed string') }.to raise_error(Parslet::ParseFailed)
|
391
|
+
expect { parser.parse('not_a_literal\n') }.to raise_error(Parslet::ParseFailed)
|
392
|
+
end
|
393
|
+
|
394
|
+
it 'handles large integers' do
|
395
|
+
input = "999999999999999999\n"
|
396
|
+
result = parser.parse(input)
|
397
|
+
expect(result[0][:literal][:integer]).to eq('999999999999999999')
|
398
|
+
end
|
399
|
+
|
400
|
+
it 'handles empty strings in mixed content' do
|
401
|
+
input = "123\n\"\"\n456\n"
|
402
|
+
result = parser.parse(input)
|
403
|
+
expect(result.length).to eq(3)
|
404
|
+
expect(result[1][:literal][:string]).to eq([])
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
2
|
+
|
3
|
+
require "parslet"
|
4
|
+
require "pp"
|
5
|
+
|
6
|
+
# Parses strings like "var1 and (var2 or var3)" respecting operator precedence
|
7
|
+
# and parentheses. After that transforms the parse tree into an array of
|
8
|
+
# arrays like this:
|
9
|
+
#
|
10
|
+
# [["1", "2"], ["1", "3"]]
|
11
|
+
#
|
12
|
+
# The array represents a DNF (disjunctive normal form). Elements of outer
|
13
|
+
# array are connected with "or" operator, while elements of inner arrays are
|
14
|
+
# joined with "and".
|
15
|
+
#
|
16
|
+
class MyParser < Parslet::Parser
|
17
|
+
rule(:space) { match[" "].repeat(1) }
|
18
|
+
rule(:space?) { space.maybe }
|
19
|
+
|
20
|
+
rule(:lparen) { str("(") >> space? }
|
21
|
+
rule(:rparen) { str(")") >> space? }
|
22
|
+
|
23
|
+
rule(:and_operator) { str("and") >> space? }
|
24
|
+
rule(:or_operator) { str("or") >> space? }
|
25
|
+
|
26
|
+
rule(:var) { str("var") >> match["0-9"].repeat(1).as(:var) >> space? }
|
27
|
+
|
28
|
+
# The primary rule deals with parentheses.
|
29
|
+
rule(:primary) { lparen >> or_operation >> rparen | var }
|
30
|
+
|
31
|
+
# Note that following rules are both right-recursive.
|
32
|
+
rule(:and_operation) {
|
33
|
+
(primary.as(:left) >> and_operator >>
|
34
|
+
and_operation.as(:right)).as(:and) |
|
35
|
+
primary }
|
36
|
+
|
37
|
+
rule(:or_operation) {
|
38
|
+
(and_operation.as(:left) >> or_operator >>
|
39
|
+
or_operation.as(:right)).as(:or) |
|
40
|
+
and_operation }
|
41
|
+
|
42
|
+
# We start at the lowest precedence rule.
|
43
|
+
root(:or_operation)
|
44
|
+
end
|
45
|
+
|
46
|
+
class Transformer < Parslet::Transform
|
47
|
+
rule(:var => simple(:var)) { [[String(var)]] }
|
48
|
+
|
49
|
+
rule(:or => { :left => subtree(:left), :right => subtree(:right) }) do
|
50
|
+
(left + right)
|
51
|
+
end
|
52
|
+
|
53
|
+
rule(:and => { :left => subtree(:left), :right => subtree(:right) }) do
|
54
|
+
res = []
|
55
|
+
left.each do |l|
|
56
|
+
right.each do |r|
|
57
|
+
res << (l + r)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
res
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# A simple integer calculator to answer the question about how to do
|
2
|
+
# left and right associativity in parslet (PEG) once and for all.
|
3
|
+
|
4
|
+
require 'parslet'
|
5
|
+
|
6
|
+
module CalcExample
|
7
|
+
# This is the parsing stage. It expresses left associativity by compiling
|
8
|
+
# list of things that have the same associativity.
|
9
|
+
class CalcParser < Parslet::Parser
|
10
|
+
root :addition
|
11
|
+
|
12
|
+
rule(:addition) {
|
13
|
+
multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
|
14
|
+
multiplication
|
15
|
+
}
|
16
|
+
|
17
|
+
rule(:multiplication) {
|
18
|
+
integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
|
19
|
+
integer }
|
20
|
+
|
21
|
+
rule(:integer) { digit.repeat(1).as(:i) >> space? }
|
22
|
+
|
23
|
+
rule(:mult_op) { match['*/'].as(:o) >> space? }
|
24
|
+
rule(:add_op) { match['+-'].as(:o) >> space? }
|
25
|
+
|
26
|
+
rule(:digit) { match['0-9'] }
|
27
|
+
rule(:space?) { match['\s'].repeat }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Classes for the abstract syntax tree.
|
31
|
+
Int = Struct.new(:int) do
|
32
|
+
def eval; self end
|
33
|
+
def op(operation, other)
|
34
|
+
left = int
|
35
|
+
right = other.int
|
36
|
+
|
37
|
+
Int.new(
|
38
|
+
case operation
|
39
|
+
when '+'
|
40
|
+
left + right
|
41
|
+
when '-'
|
42
|
+
left - right
|
43
|
+
when '*'
|
44
|
+
left * right
|
45
|
+
when '/'
|
46
|
+
left / right
|
47
|
+
end)
|
48
|
+
end
|
49
|
+
def to_i
|
50
|
+
int
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Seq = Struct.new(:sequence) do
|
55
|
+
def eval
|
56
|
+
sequence.reduce { |accum, operation|
|
57
|
+
operation.call(accum) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
LeftOp = Struct.new(:operation, :right) do
|
62
|
+
def call(left)
|
63
|
+
left = left.eval
|
64
|
+
right = self.right.eval
|
65
|
+
|
66
|
+
left.op(operation, right)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Transforming intermediary syntax tree into a real AST.
|
71
|
+
class CalcTransform < Parslet::Transform
|
72
|
+
rule(i: simple(:i)) { Int.new(Integer(i)) }
|
73
|
+
rule(o: simple(:o), r: simple(:i)) { LeftOp.new(o, i) }
|
74
|
+
rule(l: simple(:i)) { i }
|
75
|
+
rule(sequence(:seq)) { Seq.new(seq) }
|
76
|
+
end
|
77
|
+
|
78
|
+
# And this calls everything in the right order.
|
79
|
+
def self.calculate(str)
|
80
|
+
intermediary_tree = CalcParser.new.parse(str)
|
81
|
+
abstract_tree = CalcTransform.new.apply(intermediary_tree)
|
82
|
+
result = abstract_tree.eval
|
83
|
+
|
84
|
+
result.to_i
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# This example demonstrates how pieces of input can be captured and matched
|
2
|
+
# against later on. Without this, you cannot match here-documents and other
|
3
|
+
# self-dependent grammars.
|
4
|
+
|
5
|
+
require 'parslet'
|
6
|
+
|
7
|
+
module CaptureExample
|
8
|
+
class CapturingParser < Parslet::Parser
|
9
|
+
root :document
|
10
|
+
|
11
|
+
# Introduce a scope for each document. This ensures that documents can be
|
12
|
+
# nested.
|
13
|
+
rule(:document) { scope { doc_start >> text >> doc_end } }
|
14
|
+
|
15
|
+
# Start of a document is a heredoc marker. This is captured in :marker
|
16
|
+
rule(:doc_start) { str('<') >> marker >> newline }
|
17
|
+
rule(:marker) { match['A-Z'].repeat(1).capture(:marker) }
|
18
|
+
|
19
|
+
# The content of a document can be either lines of text or another
|
20
|
+
# document, introduced by <HERE, where HERE is the doc marker.
|
21
|
+
rule(:text) { (document.as(:doc) | text_line.as(:line)).repeat(1) }
|
22
|
+
rule(:text_line) { captured_marker.absent? >> any >>
|
23
|
+
(newline.absent? >> any).repeat >> newline }
|
24
|
+
|
25
|
+
# The end of the document is marked by the marker that was at the beginning
|
26
|
+
# of the document, by itself on a line.
|
27
|
+
rule(:doc_end) { captured_marker }
|
28
|
+
rule(:captured_marker) {
|
29
|
+
dynamic { |source, context|
|
30
|
+
str(context.captures[:marker])
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
rule(:newline) { match["\n"] }
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# A small example on how to parse common types of comments. The example
|
2
|
+
# started out with parser code from Stephen Waits.
|
3
|
+
|
4
|
+
require 'parslet'
|
5
|
+
|
6
|
+
module CommentsExample
|
7
|
+
class ALanguage < Parslet::Parser
|
8
|
+
root(:lines)
|
9
|
+
|
10
|
+
rule(:lines) { line.repeat }
|
11
|
+
rule(:line) { spaces >> expression.repeat >> newline }
|
12
|
+
rule(:newline) { str("\n") >> str("\r").maybe }
|
13
|
+
|
14
|
+
rule(:expression) { (str('a').as(:a) >> spaces).as(:exp) }
|
15
|
+
|
16
|
+
rule(:spaces) { space.repeat }
|
17
|
+
rule(:space) { multiline_comment | line_comment | str(' ') }
|
18
|
+
|
19
|
+
rule(:line_comment) { (str('//') >> (newline.absent? >> any).repeat).as(:line) }
|
20
|
+
rule(:multiline_comment) { (str('/*') >> (str('*/').absent? >> any).repeat >> str('*/')).as(:multi) }
|
21
|
+
end
|
22
|
+
end
|