rjq 0.1.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.
data/lib/rjq/parser.rb ADDED
@@ -0,0 +1,755 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ class Parser
5
+ DEFAULT_MAX_FILTER_DEPTH = 256
6
+ ASSIGNMENT_OPERATORS = ['=', '|=', '+=', '-=', '*=', '/=', '%=', '//='].freeze
7
+ COMPARISON_OPERATORS = %w[== != < <= > >=].freeze
8
+ ADDITIVE_OPERATORS = %w[+ -].freeze
9
+ MULTIPLICATIVE_OPERATORS = %w[* / %].freeze
10
+ PRECEDENCE = {
11
+ '|' => 1,
12
+ 'as' => 2,
13
+ ',' => 3,
14
+ '=' => 4,
15
+ '|=' => 4,
16
+ '+=' => 4,
17
+ '-=' => 4,
18
+ '*=' => 4,
19
+ '/=' => 4,
20
+ '%=' => 4,
21
+ '//=' => 4,
22
+ 'or' => 5,
23
+ 'and' => 6,
24
+ '//' => 7,
25
+ '==' => 8,
26
+ '!=' => 8,
27
+ '<' => 8,
28
+ '<=' => 8,
29
+ '>' => 8,
30
+ '>=' => 8,
31
+ '+' => 9,
32
+ '-' => 9,
33
+ '*' => 10,
34
+ '/' => 10,
35
+ '%' => 10
36
+ }.freeze
37
+
38
+ def initialize(source, allow_comments: true, source_name: '<top-level>', initial_line: 1, initial_column: 1,
39
+ start_offset: 0, max_filter_depth: DEFAULT_MAX_FILTER_DEPTH)
40
+ unless max_filter_depth.is_a?(Integer) && max_filter_depth.positive?
41
+ raise ArgumentError, 'max_filter_depth must be a positive Integer'
42
+ end
43
+
44
+ @tokens = if source.is_a?(Array)
45
+ source
46
+ else
47
+ Lexer.new(source, allow_comments: allow_comments, source_name: source_name,
48
+ initial_line: initial_line, initial_column: initial_column,
49
+ start_offset: start_offset).tokenize
50
+ end
51
+ @index = 0
52
+ @definitions = []
53
+ @max_filter_depth = max_filter_depth
54
+ @filter_depth = 0
55
+ end
56
+
57
+ def parse
58
+ directives = parse_directives
59
+ parse_definitions
60
+ body = eof? ? AST::Identity.new : parse_expression
61
+ expect(:eof)
62
+ AST::Program.new(body, @definitions, directives)
63
+ end
64
+
65
+ private
66
+
67
+ def parse_directives
68
+ directives = []
69
+ while keyword?('include') || keyword?('import') || keyword?('module')
70
+ directives << parse_directive
71
+ end
72
+ directives
73
+ end
74
+
75
+ def parse_directive
76
+ type = consume.value.to_sym
77
+ if type == :module
78
+ metadata = parse_directive_metadata
79
+ expect(:semicolon)
80
+ return AST::ModuleDirective.new(type: type, metadata: metadata)
81
+ end
82
+
83
+ path = expect(:string).value
84
+ raise error('Import path must be constant') unless path.is_a?(String)
85
+
86
+ metadata = parse_directive_metadata unless %i[semicolon eof].include?(current.type) || keyword?('as')
87
+ alias_name = parse_module_alias if type == :import
88
+ expect(:semicolon)
89
+ AST::ModuleDirective.new(type: type, name: path, metadata: metadata, alias_name: alias_name)
90
+ end
91
+
92
+ def parse_directive_metadata
93
+ case current.type
94
+ when :lbrace
95
+ parse_object_literal
96
+ when :lbracket
97
+ parse_array_literal
98
+ when :lparen
99
+ consume
100
+ value = parse_expression_allowing_comma
101
+ expect(:rparen)
102
+ value
103
+ else
104
+ parse_prefix
105
+ end
106
+ end
107
+
108
+ def parse_module_alias
109
+ expect_keyword('as')
110
+ case current.type
111
+ when :identifier
112
+ consume.value
113
+ when :variable
114
+ "$#{consume.value}"
115
+ else
116
+ raise error('expected module alias')
117
+ end
118
+ end
119
+
120
+ def parse_definitions
121
+ while keyword?('def')
122
+ name, params, body = parse_function_definition
123
+ expect(:semicolon)
124
+ @definitions << AST::FunctionDefinition.new(name, params, body)
125
+ end
126
+ end
127
+
128
+ def parse_function_definition
129
+ expect_keyword('def')
130
+ name = parse_name
131
+ params = []
132
+ if consume?(:lparen)
133
+ until current.type == :rparen
134
+ params << parse_param
135
+ break unless %i[semicolon comma].include?(current.type)
136
+
137
+ consume
138
+ end
139
+ expect(:rparen)
140
+ end
141
+ expect(:colon)
142
+ [name, params, parse_expression]
143
+ end
144
+
145
+ def parse_param
146
+ if current.type == :variable
147
+ "$#{consume.value}"
148
+ else
149
+ expect(:identifier).value
150
+ end
151
+ end
152
+
153
+ def parse_binding_pattern
154
+ case current.type
155
+ when :variable
156
+ [:var, consume.value]
157
+ when :lbrace
158
+ parse_object_binding_pattern
159
+ when :lbracket
160
+ parse_array_binding_pattern
161
+ else
162
+ raise error('expected binding pattern')
163
+ end
164
+ end
165
+
166
+ def parse_object_binding_pattern
167
+ expect(:lbrace)
168
+ pairs = []
169
+ raise error('expected binding object key') if current.type == :rbrace
170
+
171
+ until consume?(:rbrace)
172
+ key =
173
+ case current.type
174
+ when :identifier, :string, :keyword
175
+ consume.value
176
+ when :variable
177
+ variable = consume.value
178
+ pairs << if consume?(:colon)
179
+ [variable, [:both, [:var, variable], parse_binding_pattern]]
180
+ else
181
+ [variable, [:var, variable]]
182
+ end
183
+ next if consume?(:comma)
184
+
185
+ expect(:rbrace)
186
+ break
187
+ when :lparen
188
+ consume
189
+ expression = parse_expression
190
+ expect(:rparen)
191
+ expression
192
+ else
193
+ raise error('expected binding object key')
194
+ end
195
+ pairs << [key, consume?(:colon) ? parse_binding_pattern : [:var, key]]
196
+ next if consume?(:comma)
197
+
198
+ expect(:rbrace)
199
+ break
200
+ end
201
+ [:object, pairs]
202
+ end
203
+
204
+ def parse_array_binding_pattern
205
+ expect(:lbracket)
206
+ patterns = []
207
+ raise error('expected binding pattern') if current.type == :rbracket
208
+
209
+ until consume?(:rbracket)
210
+ patterns << parse_binding_pattern
211
+ next if consume?(:comma)
212
+
213
+ expect(:rbracket)
214
+ break
215
+ end
216
+ [:array, patterns]
217
+ end
218
+
219
+ def parse_expression(min_precedence = 0)
220
+ @filter_depth += 1
221
+ raise error("filter nesting exceeds #{@max_filter_depth}") if @filter_depth > @max_filter_depth
222
+
223
+ start_token = current
224
+ left = parse_prefix
225
+ loop do
226
+ op = current_operator
227
+ break unless op
228
+
229
+ precedence = PRECEDENCE[op]
230
+ break unless precedence && precedence >= min_precedence
231
+
232
+ consume
233
+ left = parse_infix(left, op, precedence)
234
+ end
235
+ add_source_span(left, start_token)
236
+ ensure
237
+ @filter_depth -= 1
238
+ end
239
+
240
+ def parse_infix(left, op, precedence)
241
+ if op == 'as'
242
+ pattern = parse_binding_pattern
243
+ alternatives = [pattern]
244
+ while current.type == :question && @tokens[@index + 1]&.type == :operator && @tokens[@index + 1]&.value == '//'
245
+ consume
246
+ consume
247
+ alternatives << parse_binding_pattern
248
+ end
249
+ pattern = [:alternatives, alternatives] if alternatives.length > 1
250
+ expect(:pipe)
251
+ return AST::Binding.new(left, pattern, parse_expression)
252
+ end
253
+
254
+ right = parse_expression(ASSIGNMENT_OPERATORS.include?(op) ? precedence : precedence + 1)
255
+ case op
256
+ when '|'
257
+ AST::Pipe.new(left, right)
258
+ when ','
259
+ AST::Comma.new(left, right)
260
+ when *ASSIGNMENT_OPERATORS
261
+ AST::Assignment.new(left, op, right)
262
+ else
263
+ AST::BinaryOp.new(left, op, right)
264
+ end
265
+ end
266
+
267
+ def parse_prefix
268
+ start_token = current
269
+ node =
270
+ case current.type
271
+ when :dot
272
+ consume
273
+ parse_dot_suffix(AST::Identity.new, required: false)
274
+ when :operator
275
+ parse_operator_prefix
276
+ when :number
277
+ AST::Literal.new(consume.value)
278
+ when :string
279
+ AST::StringLiteral.new(consume.value)
280
+ when :variable
281
+ parse_variable
282
+ when :identifier
283
+ parse_identifier
284
+ when :format
285
+ parse_format
286
+ when :keyword
287
+ parse_keyword
288
+ when :lparen
289
+ consume
290
+ expression = parse_expression_allowing_comma
291
+ expect(:rparen)
292
+ expression
293
+ when :lbracket
294
+ parse_array_literal
295
+ when :lbrace
296
+ parse_object_literal
297
+ else
298
+ raise error("unexpected token #{current.type}")
299
+ end
300
+ node = add_source_span(node, start_token) unless node.source_span
301
+ add_source_span(parse_postfix(node), start_token)
302
+ end
303
+
304
+ def parse_operator_prefix
305
+ if current.value == '-'
306
+ consume
307
+ AST::UnaryOp.new('-', parse_expression(10))
308
+ elsif current.value == '..'
309
+ consume
310
+ AST::Recurse.new
311
+ else
312
+ raise error("unexpected operator #{current.value}")
313
+ end
314
+ end
315
+
316
+ def parse_identifier
317
+ name = parse_name
318
+ return AST::FunctionCall.new(name, parse_arguments) if consume?(:lparen)
319
+
320
+ AST::FunctionCall.new(name, [])
321
+ end
322
+
323
+ def parse_variable
324
+ name = consume.value
325
+ return AST::Variable.new(name) unless current.type == :colon && @tokens[@index + 1]&.type == :colon
326
+
327
+ consume
328
+ consume
329
+ AST::FunctionCall.new("#{name}::#{parse_name}", current.type == :lparen ? parse_arguments_after_lparen : [])
330
+ end
331
+
332
+ def parse_arguments_after_lparen
333
+ expect(:lparen)
334
+ parse_arguments
335
+ end
336
+
337
+ def parse_name
338
+ name = expect(:identifier).value
339
+ while current.type == :colon && @tokens[@index + 1]&.type == :colon
340
+ consume
341
+ consume
342
+ name = "#{name}::#{expect(:identifier).value}"
343
+ end
344
+ name
345
+ end
346
+
347
+ def parse_format
348
+ name = "@#{consume.value}"
349
+ return AST::Format.new(name, parse_prefix) if current.type == :string
350
+
351
+ AST::Format.new(name)
352
+ end
353
+
354
+ def parse_keyword
355
+ case current.value
356
+ when 'null'
357
+ consume
358
+ AST::Literal.new(nil)
359
+ when 'true'
360
+ consume
361
+ AST::Literal.new(true)
362
+ when 'false'
363
+ consume
364
+ AST::Literal.new(false)
365
+ when 'if'
366
+ parse_if
367
+ when 'def'
368
+ parse_scoped_definition
369
+ when 'try'
370
+ parse_try
371
+ when 'reduce'
372
+ parse_reduce
373
+ when 'foreach'
374
+ parse_foreach
375
+ when 'label'
376
+ parse_label
377
+ when 'break'
378
+ parse_break
379
+ when 'include', 'import', 'module'
380
+ raise error('module directives are only allowed before definitions and the filter body')
381
+ when 'not'
382
+ consume
383
+ AST::FunctionCall.new('not', [])
384
+ else
385
+ raise error("unexpected keyword #{current.value}")
386
+ end
387
+ end
388
+
389
+ def parse_if
390
+ expect_keyword('if')
391
+ condition = parse_expression_allowing_pipe
392
+ expect_keyword('then')
393
+ then_branch = parse_expression_allowing_pipe
394
+ else_branch =
395
+ if keyword?('elif')
396
+ AST::If.new(parse_elif_condition, parse_elif_then, parse_elif_else)
397
+ elsif keyword?('end')
398
+ AST::Identity.new
399
+ else
400
+ expect_keyword('else')
401
+ parse_expression_allowing_pipe
402
+ end
403
+ expect_keyword('end')
404
+ AST::If.new(condition, then_branch, else_branch)
405
+ end
406
+
407
+ def parse_elif_condition
408
+ expect_keyword('elif')
409
+ parse_expression_allowing_pipe
410
+ end
411
+
412
+ def parse_elif_then
413
+ expect_keyword('then')
414
+ parse_expression_allowing_pipe
415
+ end
416
+
417
+ def parse_elif_else
418
+ if keyword?('elif')
419
+ AST::If.new(parse_elif_condition, parse_elif_then, parse_elif_else)
420
+ elsif keyword?('end')
421
+ AST::Identity.new
422
+ else
423
+ expect_keyword('else')
424
+ parse_expression_allowing_pipe
425
+ end
426
+ end
427
+
428
+ def parse_try
429
+ expect_keyword('try')
430
+ body = parse_expression
431
+ handler = nil
432
+ handler = parse_expression_stopping_at_comma_or_pipe if consume_keyword?('catch')
433
+ AST::Try.new(body, handler)
434
+ end
435
+
436
+ def parse_scoped_definition
437
+ name, params, body = parse_function_definition
438
+ expect(:semicolon)
439
+ AST::ScopedDefinition.new(AST::FunctionDefinition.new(name, params, body), parse_expression)
440
+ end
441
+
442
+ def parse_reduce
443
+ expect_keyword('reduce')
444
+ generator = parse_expression_until_keywords('as')
445
+ expect_keyword('as')
446
+ pattern = parse_binding_pattern
447
+ expect(:lparen)
448
+ initial = parse_expression
449
+ expect(:semicolon)
450
+ update = parse_expression
451
+ expect(:rparen)
452
+ AST::Reduce.new(generator, pattern, initial, update)
453
+ end
454
+
455
+ def parse_foreach
456
+ expect_keyword('foreach')
457
+ generator = parse_expression_until_keywords('as')
458
+ expect_keyword('as')
459
+ pattern = parse_binding_pattern
460
+ expect(:lparen)
461
+ initial = parse_expression
462
+ expect(:semicolon)
463
+ update = parse_expression
464
+ extract = consume?(:semicolon) ? parse_expression : nil
465
+ expect(:rparen)
466
+ AST::Foreach.new(generator, pattern, initial, update, extract)
467
+ end
468
+
469
+ def parse_label
470
+ expect_keyword('label')
471
+ label = expect(:variable).value
472
+ expect(:pipe)
473
+ AST::Label.new(label, parse_expression)
474
+ end
475
+
476
+ def parse_break
477
+ expect_keyword('break')
478
+ AST::Break.new(expect(:variable).value)
479
+ end
480
+
481
+ def parse_arguments
482
+ args = []
483
+ until consume?(:rparen)
484
+ args << parse_expression
485
+ next if consume?(:semicolon)
486
+
487
+ expect(:rparen)
488
+ break
489
+ end
490
+ args
491
+ end
492
+
493
+ def parse_array_literal
494
+ expect(:lbracket)
495
+ return AST::ArrayLiteral.new(nil) if consume?(:rbracket)
496
+
497
+ expression = parse_expression_allowing_comma
498
+ expect(:rbracket)
499
+ AST::ArrayLiteral.new(expression)
500
+ end
501
+
502
+ def parse_object_literal
503
+ expect(:lbrace)
504
+ pairs = []
505
+ until consume?(:rbrace)
506
+ pairs << parse_object_pair
507
+ next if consume?(:comma)
508
+
509
+ expect(:rbrace)
510
+ break
511
+ end
512
+ AST::ObjectLiteral.new(pairs)
513
+ end
514
+
515
+ def parse_object_pair
516
+ case current.type
517
+ when :identifier, :keyword
518
+ key = consume.value
519
+ value = consume?(:colon) ? parse_expression_stopping_at_comma : AST::Field.new(AST::Identity.new, key)
520
+ when :string
521
+ key = consume.value
522
+ value =
523
+ if consume?(:colon)
524
+ parse_expression_stopping_at_comma
525
+ elsif key.is_a?(Array)
526
+ AST::Index.new(AST::Identity.new, AST::StringLiteral.new(key))
527
+ else
528
+ AST::Field.new(AST::Identity.new, key)
529
+ end
530
+ when :variable
531
+ variable = consume.value
532
+ if consume?(:colon)
533
+ key = AST::Variable.new(variable)
534
+ value = parse_expression_stopping_at_comma
535
+ else
536
+ key = variable
537
+ value = AST::Variable.new(variable)
538
+ end
539
+ when :lparen
540
+ consume
541
+ key = parse_expression
542
+ expect(:rparen)
543
+ expect(:colon)
544
+ value = parse_expression_stopping_at_comma
545
+ else
546
+ raise error('expected object key')
547
+ end
548
+ AST::ObjectLiteral::Pair.new(key: key, value: value)
549
+ end
550
+
551
+ def parse_postfix(node)
552
+ loop do
553
+ node =
554
+ case current.type
555
+ when :dot
556
+ consume
557
+ parse_dot_suffix(node, required: true)
558
+ when :lbracket
559
+ parse_bracket_access(node)
560
+ when :question
561
+ consume
562
+ AST::Optional.new(node)
563
+ else
564
+ return node
565
+ end
566
+ end
567
+ end
568
+
569
+ def parse_dot_suffix(base, required:)
570
+ case current.type
571
+ when :identifier
572
+ AST::Field.new(base, consume.value)
573
+ when :keyword
574
+ return base unless token_adjacent_to_previous?
575
+
576
+ AST::Field.new(base, consume.value)
577
+ when :string
578
+ value = consume.value
579
+ value.is_a?(Array) ? AST::Index.new(base, AST::StringLiteral.new(value)) : AST::Field.new(base, value)
580
+ when :lbracket
581
+ parse_bracket_access(base)
582
+ else
583
+ raise error('expected field or bracket expression after dot') if required
584
+
585
+ base
586
+ end
587
+ end
588
+
589
+ def token_adjacent_to_previous?
590
+ previous.line == current.line && previous.column + previous.value.to_s.length == current.column
591
+ end
592
+
593
+ def parse_bracket_access(base)
594
+ expect(:lbracket)
595
+ return AST::Iterate.new(base) if consume?(:rbracket)
596
+
597
+ if consume?(:colon)
598
+ if consume?(:rbracket)
599
+ finish_node = nil
600
+ else
601
+ finish_node = parse_expression(PRECEDENCE.fetch(',') + 1)
602
+ expect(:rbracket)
603
+ end
604
+ return AST::Slice.new(base, nil, finish_node)
605
+ end
606
+
607
+ start_node = parse_expression_allowing_comma
608
+ if consume?(:colon)
609
+ if consume?(:rbracket)
610
+ finish_node = nil
611
+ else
612
+ finish_node = parse_expression(PRECEDENCE.fetch(',') + 1)
613
+ expect(:rbracket)
614
+ end
615
+ AST::Slice.new(base, start_node, finish_node)
616
+ else
617
+ expect(:rbracket)
618
+ AST::Index.new(base, start_node)
619
+ end
620
+ end
621
+
622
+ def current_operator
623
+ return nil if stopped_keyword?
624
+
625
+ return '|' if current.type == :pipe && !stop_at_pipe?
626
+ return ',' if current.type == :comma && !stop_at_comma?
627
+ return current.value if current.type == :operator
628
+ return current.value if current.type == :keyword && %w[as and or].include?(current.value)
629
+
630
+ nil
631
+ end
632
+
633
+ def parse_expression_stopping_at_comma
634
+ @stop_at_comma = @stop_at_comma.to_i + 1
635
+ parse_expression
636
+ ensure
637
+ @stop_at_comma -= 1
638
+ end
639
+
640
+ def parse_expression_stopping_at_comma_or_pipe
641
+ @stop_at_comma = @stop_at_comma.to_i + 1
642
+ @stop_at_pipe = @stop_at_pipe.to_i + 1
643
+ parse_expression
644
+ ensure
645
+ @stop_at_comma -= 1
646
+ @stop_at_pipe -= 1
647
+ end
648
+
649
+ def parse_expression_allowing_comma
650
+ previous = @stop_at_comma
651
+ @stop_at_comma = 0
652
+ parse_expression
653
+ ensure
654
+ @stop_at_comma = previous
655
+ end
656
+
657
+ def parse_expression_allowing_pipe
658
+ previous = @stop_at_pipe
659
+ @stop_at_pipe = 0
660
+ parse_expression
661
+ ensure
662
+ @stop_at_pipe = previous
663
+ end
664
+
665
+ def parse_expression_until_keywords(*keywords)
666
+ @stop_keywords ||= []
667
+ @stop_keywords.push(keywords.flatten)
668
+ parse_expression
669
+ ensure
670
+ @stop_keywords.pop
671
+ end
672
+
673
+ def stop_at_comma?
674
+ @stop_at_comma.to_i.positive?
675
+ end
676
+
677
+ def stop_at_pipe?
678
+ @stop_at_pipe.to_i.positive?
679
+ end
680
+
681
+ def stopped_keyword?
682
+ current.type == :keyword && @stop_keywords&.any? { |keywords| keywords.include?(current.value) }
683
+ end
684
+
685
+ def expect(type)
686
+ raise error("expected #{type}, got #{current.type}") unless current.type == type
687
+
688
+ consume
689
+ end
690
+
691
+ def expect_keyword(value)
692
+ raise error("expected #{value}") unless keyword?(value)
693
+
694
+ consume
695
+ end
696
+
697
+ def consume?(type)
698
+ return false unless current.type == type
699
+
700
+ consume
701
+ true
702
+ end
703
+
704
+ def consume_keyword?(value)
705
+ return false unless keyword?(value)
706
+
707
+ consume
708
+ true
709
+ end
710
+
711
+ def consume_until_semicolon
712
+ consume until %i[semicolon eof].include?(current.type)
713
+ consume?(:semicolon)
714
+ end
715
+
716
+ def keyword?(value)
717
+ current.type == :keyword && current.value == value
718
+ end
719
+
720
+ def consume
721
+ token = current
722
+ @index += 1
723
+ token
724
+ end
725
+
726
+ def previous
727
+ @tokens[@index - 1]
728
+ end
729
+
730
+ def current
731
+ @tokens[@index]
732
+ end
733
+
734
+ def eof?
735
+ current.type == :eof
736
+ end
737
+
738
+ def error(message)
739
+ ParseError.new("#{message} at #{current.filename || '<top-level>'}, line #{current.line}, column #{current.column}")
740
+ end
741
+
742
+ def add_source_span(node, start_token)
743
+ return node unless node.is_a?(AST::Node)
744
+
745
+ finish_token = previous || start_token
746
+ node.with_source_span(AST::SourceSpan.new(
747
+ filename: start_token.filename || '<top-level>',
748
+ line: start_token.line,
749
+ column: start_token.column,
750
+ start_offset: start_token.start_offset,
751
+ end_offset: finish_token.end_offset
752
+ ))
753
+ end
754
+ end
755
+ end