arpaka 1.0.0 → 2.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 +4 -4
- data/CHANGELOG.md +46 -2
- data/README.md +104 -7
- data/Rakefile +15 -1
- data/docs/architecture.dot +30 -0
- data/docs/architecture.md +96 -0
- data/docs/architecture.webp +0 -0
- data/docs/contributes.md +115 -0
- data/docs/usage.md +497 -0
- data/lib/arpaka/cli.rb +5 -53
- data/lib/arpaka/plugins/cli.rb +61 -0
- data/lib/arpaka/plugins/sample.rb +14 -0
- data/lib/arpaka/plugins/sample_runner.rb +21 -0
- data/lib/arpaka/{actions.rb → ruby/actions.rb} +230 -151
- data/lib/arpaka/ruby/class_name_parser.rb +97 -0
- data/lib/arpaka/ruby/cli.rb +64 -0
- data/lib/arpaka/ruby/frontend_emitter.rb +59 -0
- data/lib/arpaka/ruby/generator.rb +41 -0
- data/lib/arpaka/{ruby_grammar.rb → ruby/grammar.rb} +5 -56
- data/lib/arpaka/ruby/grammar_emitter.rb +71 -0
- data/lib/arpaka/ruby/profile.rb +129 -0
- data/lib/arpaka/{profile → ruby/profiles}/ruby_3_3.rb +21 -21
- data/lib/arpaka/{profile → ruby/profiles}/ruby_3_4.rb +21 -21
- data/lib/arpaka/{runtime → ruby/runtime/detailed}/ast.rb.erb +41 -5
- data/lib/arpaka/ruby/runtime/detailed/builder.rb.erb +1177 -0
- data/lib/arpaka/{runtime → ruby/runtime}/frontend.rb.erb +17 -4
- data/lib/arpaka/{runtime → ruby/runtime}/lexer.rb.erb +576 -99
- data/lib/arpaka/ruby/runtime/structured.rb.erb +111 -0
- data/lib/arpaka/ruby.rb +23 -0
- data/lib/arpaka.rb +6 -9
- data/lib/lrama/ruby/backend.rb +4 -9
- data/lib/lrama/ruby/parser.rb.erb +35 -9
- data/lib/lrama/ruby/parser_emitter.rb +41 -0
- data/lib/lrama/ruby/table_codec.rb +50 -0
- data/lib/lrama/ruby/version.rb +1 -1
- data/lib/lrama/ruby.rb +1 -0
- data/sig/arpaka.rbs +8 -3
- metadata +32 -16
- data/lib/arpaka/generator.rb +0 -51
- data/lib/arpaka/profile/ruby.rb +0 -49
- data/lib/arpaka/runtime/builder.rb.erb +0 -323
- /data/lib/arpaka/{BSDL → ruby/BSDL} +0 -0
- /data/lib/arpaka/{COPYING → ruby/COPYING} +0 -0
- /data/lib/arpaka/{source.json → ruby/source.json} +0 -0
- /data/lib/arpaka/{upstream_rules.json → ruby/upstream_rules.json} +0 -0
|
@@ -5,7 +5,8 @@ class LexicalContext
|
|
|
5
5
|
attr_accessor :begin_expression, :condition_do, :condition_line,
|
|
6
6
|
:ternary_depth, :lambda_pending, :alias_context, :argument_label,
|
|
7
7
|
:lex_state, :command_start, :label_pending, :singleton_class_depth, :control_depth,
|
|
8
|
-
:method_header, :method_body_start
|
|
8
|
+
:method_header, :method_body_start, :command_argument_candidate,
|
|
9
|
+
:command_argument_pending, :command_call_active, :command_call_argument
|
|
9
10
|
attr_reader :delimiter_stack, :cmdarg_stack, :condition_stack,
|
|
10
11
|
:token_history, :state_history, :block_stack, :scope_stack, :parser_events,
|
|
11
12
|
:parser_delimiter_stack, :parser_cmdarg_stack, :parser_block_stack,
|
|
@@ -24,6 +25,10 @@ class LexicalContext
|
|
|
24
25
|
@control_depth = 0
|
|
25
26
|
@method_header = false
|
|
26
27
|
@method_body_start = false
|
|
28
|
+
@command_argument_candidate = false
|
|
29
|
+
@command_argument_pending = false
|
|
30
|
+
@command_call_active = false
|
|
31
|
+
@command_call_argument = false
|
|
27
32
|
@lex_state = :expr_beg
|
|
28
33
|
@command_start = true
|
|
29
34
|
@delimiter_stack = []
|
|
@@ -185,7 +190,7 @@ end
|
|
|
185
190
|
class Lexer
|
|
186
191
|
Error = LexerError
|
|
187
192
|
|
|
188
|
-
attr_reader :context
|
|
193
|
+
attr_reader :context, :source_encoding
|
|
189
194
|
|
|
190
195
|
KEYWORDS = {
|
|
191
196
|
"class" => :keyword_class, "module" => :keyword_module,
|
|
@@ -220,17 +225,18 @@ class Lexer
|
|
|
220
225
|
"=>" => :tASSOC, "->" => :tLAMBDA, "[]" => :tAREF, "[]=" => :tASET
|
|
221
226
|
}.freeze
|
|
222
227
|
|
|
223
|
-
def initialize(source, filename: "(ruby)")
|
|
228
|
+
def initialize(source, filename: "(ruby)", initial_line: 1)
|
|
224
229
|
unless source.is_a?(String)
|
|
225
230
|
raise ArgumentError, "source must be a String"
|
|
226
231
|
end
|
|
227
232
|
unless [Encoding::UTF_8, Encoding::US_ASCII].include?(source.encoding) && source.valid_encoding?
|
|
228
233
|
raise Error, "#{filename}:1:0: invalid UTF-8 source"
|
|
229
234
|
end
|
|
235
|
+
@source_encoding = source_encoding_for(source)
|
|
230
236
|
@source = source.b
|
|
231
237
|
@filename = filename
|
|
232
238
|
@index = 0
|
|
233
|
-
@line =
|
|
239
|
+
@line = initial_line
|
|
234
240
|
@column = 0
|
|
235
241
|
@previous_previous = nil
|
|
236
242
|
@previous = nil
|
|
@@ -239,9 +245,30 @@ class Lexer
|
|
|
239
245
|
@pending = []
|
|
240
246
|
@heredoc_queue = []
|
|
241
247
|
@deferred_suffix = nil
|
|
248
|
+
@deferred_suffix_line = nil
|
|
242
249
|
@heredoc_newline_pending = false
|
|
243
250
|
@class_superclass = false
|
|
244
251
|
@local_variables = {}
|
|
252
|
+
@source_local_variables = source_local_variable_names
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def source_encoding_for(source)
|
|
256
|
+
lines = source.lines.first(2)
|
|
257
|
+
lines.each_with_index do |line, index|
|
|
258
|
+
next if index == 1 && !lines.first.start_with?("#!")
|
|
259
|
+
|
|
260
|
+
match = line.match(/\A[ \t]*#.*?(?:coding|encoding)\s*[:=]\s*([A-Za-z0-9._-]+)/i)
|
|
261
|
+
next unless match
|
|
262
|
+
|
|
263
|
+
return Encoding.find(match[1]).name
|
|
264
|
+
end
|
|
265
|
+
source.encoding.name
|
|
266
|
+
rescue ArgumentError => error
|
|
267
|
+
raise Error, "invalid source encoding: #{error.message}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def source_string
|
|
271
|
+
String.new(encoding: @source_encoding)
|
|
245
272
|
end
|
|
246
273
|
|
|
247
274
|
def each
|
|
@@ -286,7 +313,8 @@ class Lexer
|
|
|
286
313
|
elsif @pending.empty? && @deferred_suffix
|
|
287
314
|
suffix = @deferred_suffix
|
|
288
315
|
@deferred_suffix = nil
|
|
289
|
-
@pending.concat(heredoc_suffix_tokens(suffix))
|
|
316
|
+
@pending.concat(heredoc_suffix_tokens(suffix, @deferred_suffix_line))
|
|
317
|
+
@deferred_suffix_line = nil
|
|
290
318
|
@pending.pop if @pending.last == [0, nil]
|
|
291
319
|
@pending << ["\n", nil]
|
|
292
320
|
@heredoc_newline_pending = false
|
|
@@ -498,17 +526,50 @@ class Lexer
|
|
|
498
526
|
end
|
|
499
527
|
|
|
500
528
|
def update_block_context(token)
|
|
529
|
+
if ["=", :tASSOC, :tOP_ASGN].include?(token)
|
|
530
|
+
@context.command_call_active = false
|
|
531
|
+
@context.command_call_argument = false
|
|
532
|
+
end
|
|
533
|
+
if token == ")" && @context.command_argument_candidate
|
|
534
|
+
@context.command_argument_candidate = false
|
|
535
|
+
@context.command_argument_pending = true
|
|
536
|
+
elsif @context.command_argument_pending &&
|
|
537
|
+
![:keyword_do, :keyword_do_block, :keyword_do_LAMBDA].include?(token)
|
|
538
|
+
@context.command_argument_pending = false
|
|
539
|
+
elsif @context.command_argument_candidate &&
|
|
540
|
+
![".", :tCOLON2, :tANDDOT, "(", :tLPAREN, :tLPAREN_ARG,
|
|
541
|
+
:tIDENTIFIER, :tCONSTANT, :tFID].include?(token) &&
|
|
542
|
+
!@context.delimiter_stack.include?("(")
|
|
543
|
+
@context.command_argument_candidate = false
|
|
544
|
+
end
|
|
501
545
|
case token
|
|
546
|
+
when "("
|
|
547
|
+
unless @context.command_call_argument
|
|
548
|
+
@context.command_call_active = false
|
|
549
|
+
end
|
|
550
|
+
when ","
|
|
551
|
+
@context.command_call_argument = true if @context.command_call_active &&
|
|
552
|
+
@context.delimiter_stack.empty? &&
|
|
553
|
+
(@context.block_stack.empty? || @context.block_body_at_current_delimiter?)
|
|
554
|
+
when "\n", ";"
|
|
555
|
+
@context.command_argument_candidate = false
|
|
556
|
+
@context.command_argument_pending = false
|
|
557
|
+
@context.command_call_active = false
|
|
558
|
+
@context.command_call_argument = false
|
|
559
|
+
when :keyword_do, :keyword_do_block
|
|
560
|
+
@context.command_call_active = false
|
|
561
|
+
@context.command_call_argument = false
|
|
562
|
+
@context.push_block(:do_block)
|
|
563
|
+
when :keyword_do_LAMBDA
|
|
564
|
+
@context.command_call_active = false
|
|
565
|
+
@context.command_call_argument = false
|
|
566
|
+
@context.push_block(:lambda)
|
|
502
567
|
when :keyword_if, :keyword_unless, :keyword_case, :keyword_begin,
|
|
503
568
|
:keyword_while, :keyword_until, :keyword_for
|
|
504
569
|
@context.control_depth += 1
|
|
505
570
|
when :keyword_def
|
|
506
571
|
@context.method_header = true
|
|
507
572
|
@context.push_scope(:method)
|
|
508
|
-
when :keyword_do, :keyword_do_block
|
|
509
|
-
@context.push_block(:do_block)
|
|
510
|
-
when :keyword_do_LAMBDA
|
|
511
|
-
@context.push_block(:lambda)
|
|
512
573
|
when :tLAMBDA
|
|
513
574
|
@context.push_scope(:lambda)
|
|
514
575
|
when :tLSHFT
|
|
@@ -516,8 +577,12 @@ class Lexer
|
|
|
516
577
|
when :tLAMBEG
|
|
517
578
|
@context.push_block(:lambda)
|
|
518
579
|
when "{", :tLBRACE_ARG
|
|
580
|
+
@context.command_call_active = false
|
|
581
|
+
@context.command_call_argument = false
|
|
519
582
|
@context.push_block(:brace_block)
|
|
520
583
|
when "}"
|
|
584
|
+
@context.command_call_active = false
|
|
585
|
+
@context.command_call_argument = false
|
|
521
586
|
@context.pop_block if [:brace_block, :lambda].include?(@context.block_stack.last)
|
|
522
587
|
@context.pop_scope if @context.scope_stack.last == :lambda
|
|
523
588
|
when :keyword_end
|
|
@@ -549,6 +614,25 @@ class Lexer
|
|
|
549
614
|
advance
|
|
550
615
|
end
|
|
551
616
|
word = @source.byteslice(start, @index - start).force_encoding(Encoding::UTF_8)
|
|
617
|
+
if word == "__END__" && end_data_marker?(start)
|
|
618
|
+
@index = @source.bytesize
|
|
619
|
+
return nil
|
|
620
|
+
end
|
|
621
|
+
command_start = @context.command_start ||
|
|
622
|
+
["=", :tASSOC, :tOP_ASGN].include?(@previous)
|
|
623
|
+
if command_start
|
|
624
|
+
@context.command_call_active = true
|
|
625
|
+
@context.command_call_argument = false
|
|
626
|
+
elsif word != "do" && @context.command_call_active && !@context.command_call_argument &&
|
|
627
|
+
start.positive? && [9, 32].include?(@source.getbyte(start - 1)) &&
|
|
628
|
+
[:tIDENTIFIER, :tCONSTANT, :tFID].include?(@previous)
|
|
629
|
+
@context.command_call_argument = true
|
|
630
|
+
end
|
|
631
|
+
if !@context.command_start && [:tIDENTIFIER, :tCONSTANT, :tFID].include?(@previous) &&
|
|
632
|
+
start.positive? && [9, 32].include?(@source.getbyte(start - 1)) &&
|
|
633
|
+
nested_parenthesized_command_argument?
|
|
634
|
+
@context.command_argument_candidate = true
|
|
635
|
+
end
|
|
552
636
|
if byte == 63 && word == "defined"
|
|
553
637
|
advance
|
|
554
638
|
word = "defined?"
|
|
@@ -579,8 +663,9 @@ class Lexer
|
|
|
579
663
|
advance
|
|
580
664
|
return [:tIDENTIFIER, (word + "=").to_sym]
|
|
581
665
|
end
|
|
582
|
-
@local_variables[word.to_sym] = true if
|
|
583
|
-
|
|
666
|
+
@local_variables[word.to_sym] = true if @context.lambda_pending ||
|
|
667
|
+
(byte == 61 && byte(1) != 61 &&
|
|
668
|
+
![".", :tCOLON2, :tANDDOT].include?(@previous))
|
|
584
669
|
label_in_ternary = ["(", "[", :tLPAREN, :tLPAREN_ARG, :tLBRACK, ","].include?(@previous)
|
|
585
670
|
if byte == 58 && byte(1) != 58 &&
|
|
586
671
|
(@context.ternary_depth.zero? || label_in_ternary) &&
|
|
@@ -615,7 +700,7 @@ class Lexer
|
|
|
615
700
|
@context.push_condition
|
|
616
701
|
end
|
|
617
702
|
@context.alias_context = true if token == :keyword_alias
|
|
618
|
-
[token, word.to_sym]
|
|
703
|
+
[token, word == "__LINE__" ? @line : word.to_sym]
|
|
619
704
|
rescue EncodingError
|
|
620
705
|
fail!("invalid UTF-8 identifier", start)
|
|
621
706
|
end
|
|
@@ -624,6 +709,17 @@ class Lexer
|
|
|
624
709
|
value && (value == 95 || value.between?(65, 90) || value.between?(97, 122) || value >= 128 || value.between?(48, 57))
|
|
625
710
|
end
|
|
626
711
|
|
|
712
|
+
def nested_parenthesized_command_argument?
|
|
713
|
+
position = @index
|
|
714
|
+
position += 1 while [9, 32].include?(@source.getbyte(position))
|
|
715
|
+
if @source.getbyte(position) == 46
|
|
716
|
+
position += 1
|
|
717
|
+
position += 1 while identifier_byte?(@source.getbyte(position))
|
|
718
|
+
position += 1 while [9, 32].include?(@source.getbyte(position))
|
|
719
|
+
end
|
|
720
|
+
@source.getbyte(position) == 40
|
|
721
|
+
end
|
|
722
|
+
|
|
627
723
|
def do_token
|
|
628
724
|
if @context.condition_do && @context.condition?
|
|
629
725
|
@context.pop_condition
|
|
@@ -636,30 +732,48 @@ class Lexer
|
|
|
636
732
|
:keyword_do
|
|
637
733
|
elsif @previous == ")" && @context.token_history.each_cons(2).any? { |left, right| left == :tUMINUS && right == :tLPAREN }
|
|
638
734
|
:keyword_do_block
|
|
735
|
+
elsif @previous == ")" && @context.command_argument_pending
|
|
736
|
+
@context.command_argument_pending = false
|
|
737
|
+
:keyword_do_block
|
|
738
|
+
elsif command_argument_receiver_call?
|
|
739
|
+
:keyword_do_block
|
|
740
|
+
elsif @context.command_call_active && @context.command_call_argument &&
|
|
741
|
+
@previous == :tIDENTIFIER && [:lambda, :proc].include?(@previous_value)
|
|
742
|
+
:keyword_do
|
|
743
|
+
elsif @previous == :tIDENTIFIER && @previous_previous == "." && @context.token_history[-3] == ")"
|
|
744
|
+
:keyword_do
|
|
745
|
+
elsif @previous == ")" && @context.command_call_active && @context.command_call_argument
|
|
746
|
+
:keyword_do_block
|
|
639
747
|
elsif [")", :keyword_super, :keyword_yield].include?(@previous)
|
|
640
748
|
:keyword_do
|
|
641
749
|
elsif @previous == :tLAMBDA
|
|
642
750
|
:keyword_do_LAMBDA
|
|
751
|
+
elsif @context.command_call_active && @context.command_call_argument
|
|
752
|
+
:keyword_do_block
|
|
643
753
|
else
|
|
644
754
|
:keyword_do_block
|
|
645
755
|
end
|
|
646
756
|
end
|
|
647
757
|
|
|
648
758
|
def no_argument_block?
|
|
759
|
+
return false if @context.command_call_active && @context.command_call_argument
|
|
760
|
+
return false if command_argument_receiver_call?
|
|
649
761
|
return false unless [:tIDENTIFIER, :tCONSTANT, :tFID].include?(@previous)
|
|
650
762
|
return false if @context.argument_label == :at && [".", :tCOLON2, :tANDDOT].include?(@previous_previous)
|
|
651
763
|
return false if @context.token_history == ["+", :tINTEGER, ".", :tIDENTIFIER]
|
|
652
764
|
return false if [:tIDENTIFIER, :tCONSTANT, :tFID].include?(@previous_previous)
|
|
653
765
|
return false if [:tSYMBEG, :tLABEL, :tCOLON2].include?(@previous_previous)
|
|
766
|
+
return true if @previous_value == :lambda
|
|
767
|
+
return false if @previous_previous == "," && @context.delimiter_stack.empty? &&
|
|
768
|
+
[:tIDENTIFIER, :tCONSTANT, :tFID, :tINTEGER, :tFLOAT, :tRATIONAL, :tIMAGINARY].include?(@context.token_history[-3])
|
|
654
769
|
return true if @previous_value == :lambda && @previous_previous == :tLSHFT
|
|
655
770
|
return false if [:tLSHFT, :tLAMBDA].include?(@previous_previous)
|
|
656
771
|
true
|
|
657
772
|
end
|
|
658
773
|
|
|
659
|
-
def
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
no_argument_block?
|
|
774
|
+
def command_argument_receiver_call?
|
|
775
|
+
@context.command_call_active && @previous == :tIDENTIFIER && @previous_previous == "." &&
|
|
776
|
+
[:tINTEGER, :tFLOAT, :tRATIONAL, :tIMAGINARY].include?(@context.token_history[-3])
|
|
663
777
|
end
|
|
664
778
|
|
|
665
779
|
def number_token(start)
|
|
@@ -692,7 +806,8 @@ class Lexer
|
|
|
692
806
|
text = @source.byteslice(start, @index - start).delete("_")
|
|
693
807
|
return [:tRATIONAL, Rational(text.delete_suffix("r"))] if suffix == 114
|
|
694
808
|
return [:tIMAGINARY, Complex(0, text.delete_suffix("i").to_f)] if suffix == 105
|
|
695
|
-
|
|
809
|
+
base = 8 if !float && text.start_with?("0")
|
|
810
|
+
[float ? :tFLOAT : :tINTEGER, float ? text.to_f : Integer(text, base)]
|
|
696
811
|
rescue ArgumentError, ZeroDivisionError
|
|
697
812
|
fail!("invalid numeric literal", start)
|
|
698
813
|
end
|
|
@@ -706,7 +821,7 @@ class Lexer
|
|
|
706
821
|
marker = byte
|
|
707
822
|
advance
|
|
708
823
|
advance if marker == 64 && byte == 64
|
|
709
|
-
if marker == 36 && [33, 38, 39, 42, 43, 46, 47, 58, 60, 62, 61, 63, 96, 126].include?(byte)
|
|
824
|
+
if marker == 36 && [33, 38, 39, 42, 43, 44, 46, 47, 58, 60, 62, 61, 63, 96, 126].include?(byte)
|
|
710
825
|
advance
|
|
711
826
|
text = @source.byteslice(start, @index - start)
|
|
712
827
|
return [:tGVAR, text.to_sym]
|
|
@@ -716,6 +831,10 @@ class Lexer
|
|
|
716
831
|
return [:tGVAR, :"$$"]
|
|
717
832
|
end
|
|
718
833
|
if marker == 36 && byte && byte.between?(48, 57)
|
|
834
|
+
if byte == 48
|
|
835
|
+
advance
|
|
836
|
+
return [:tGVAR, :"$0"]
|
|
837
|
+
end
|
|
719
838
|
advance while byte && byte.between?(48, 57)
|
|
720
839
|
return [:tNTH_REF, @source.byteslice(start + 1, @index - start - 1).to_i]
|
|
721
840
|
end
|
|
@@ -796,6 +915,27 @@ class Lexer
|
|
|
796
915
|
fail!("invalid UTF-8 character")
|
|
797
916
|
end
|
|
798
917
|
|
|
918
|
+
def source_character
|
|
919
|
+
length = utf8_character_length(byte)
|
|
920
|
+
value = @source.byteslice(@index, length).dup.force_encoding(Encoding::UTF_8)
|
|
921
|
+
advance(length)
|
|
922
|
+
value
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def escaped_character
|
|
926
|
+
value = byte
|
|
927
|
+
if value >= 0x80
|
|
928
|
+
source_character
|
|
929
|
+
else
|
|
930
|
+
advance
|
|
931
|
+
value.chr.force_encoding(Encoding::UTF_8)
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def append_source_character(result)
|
|
936
|
+
result << source_character
|
|
937
|
+
end
|
|
938
|
+
|
|
799
939
|
def symbol_or_colon(start)
|
|
800
940
|
symbol_position = @context.begin_expression || ([:tIDENTIFIER, :tCONSTANT, :tFID].include?(@previous) && @context.ternary_depth.zero?)
|
|
801
941
|
return operator_or_punctuation(start) unless symbol_position && byte(1) &&
|
|
@@ -811,6 +951,14 @@ class Lexer
|
|
|
811
951
|
end
|
|
812
952
|
|
|
813
953
|
def string_token(quote, start)
|
|
954
|
+
# A receiver call can be the final expression of a command-call argument:
|
|
955
|
+
# `test "description".squish do ... end`. Mark the string as an
|
|
956
|
+
# argument before consuming the receiver call so `do` binds to `test`,
|
|
957
|
+
# rather than to `squish`.
|
|
958
|
+
if @context.command_call_active && !@context.command_call_argument &&
|
|
959
|
+
@context.delimiter_stack.empty? && @context.block_stack.empty?
|
|
960
|
+
@context.command_call_argument = true
|
|
961
|
+
end
|
|
814
962
|
advance
|
|
815
963
|
content_tokens = read_interpolated_quoted(quote, start)
|
|
816
964
|
label = byte == 58 && @context.ternary_depth.zero?
|
|
@@ -823,29 +971,34 @@ class Lexer
|
|
|
823
971
|
|
|
824
972
|
def read_interpolated_quoted(quote, start)
|
|
825
973
|
return [[:tSTRING_CONTENT, read_quoted(quote, interpolate: false, start: start)]] if quote == 39
|
|
826
|
-
content =
|
|
974
|
+
content = source_string
|
|
827
975
|
tokens = []
|
|
828
976
|
until eof?
|
|
829
977
|
value = byte
|
|
830
978
|
if value == quote
|
|
831
979
|
advance
|
|
832
|
-
tokens << [:tSTRING_CONTENT, content]
|
|
980
|
+
tokens << [:tSTRING_CONTENT, content] if tokens.empty? || !content.empty?
|
|
833
981
|
return tokens
|
|
834
982
|
elsif value == 92
|
|
835
|
-
content << escape_sequence(start)
|
|
983
|
+
content << escape_sequence(start, mode: :double, quote: quote)
|
|
836
984
|
elsif value == 35 && byte(1) == 123
|
|
837
985
|
tokens << [:tSTRING_CONTENT, content] unless content.empty?
|
|
838
|
-
content =
|
|
986
|
+
content = source_string
|
|
839
987
|
advance(2)
|
|
988
|
+
expression_line = @line
|
|
840
989
|
expression = read_interpolation_source(start)
|
|
841
|
-
inner = self.class.new(expression, filename: @filename).each.to_a
|
|
990
|
+
inner = self.class.new(expression, filename: @filename, initial_line: expression_line).each.to_a
|
|
842
991
|
inner.pop if inner.last == [0, nil]
|
|
992
|
+
inner.map! { |token| token.first == "\n" ? [";", nil] : token }
|
|
843
993
|
tokens << [:tSTRING_DBEG, nil]
|
|
844
994
|
tokens.concat(inner)
|
|
845
995
|
tokens << [:tSTRING_DEND, nil]
|
|
996
|
+
elsif value == 35 && (length = shorthand_interpolation_length)
|
|
997
|
+
tokens << [:tSTRING_CONTENT, content] unless content.empty?
|
|
998
|
+
content = source_string
|
|
999
|
+
tokens.concat(shorthand_interpolation_tokens(start, length))
|
|
846
1000
|
else
|
|
847
|
-
content
|
|
848
|
-
advance
|
|
1001
|
+
append_source_character(content)
|
|
849
1002
|
end
|
|
850
1003
|
end
|
|
851
1004
|
fail!("unterminated literal", start)
|
|
@@ -884,46 +1037,173 @@ class Lexer
|
|
|
884
1037
|
end
|
|
885
1038
|
|
|
886
1039
|
def read_quoted(quote, interpolate:, start:)
|
|
887
|
-
result =
|
|
1040
|
+
result = source_string
|
|
888
1041
|
until eof?
|
|
889
1042
|
value = byte
|
|
890
1043
|
if value == quote
|
|
891
1044
|
advance
|
|
892
1045
|
return result
|
|
893
1046
|
elsif value == 92
|
|
894
|
-
|
|
1047
|
+
mode = quote == 39 ? :single : :double
|
|
1048
|
+
result << escape_sequence(start, mode: mode, quote: quote)
|
|
895
1049
|
elsif value == 35 && interpolate && byte(1) == 123
|
|
896
1050
|
fail!("string interpolation cannot be represented by the AST", start)
|
|
897
1051
|
else
|
|
898
|
-
result
|
|
899
|
-
advance
|
|
1052
|
+
append_source_character(result)
|
|
900
1053
|
end
|
|
901
1054
|
end
|
|
902
1055
|
fail!("unterminated literal", start)
|
|
903
1056
|
end
|
|
904
1057
|
|
|
905
|
-
def escape_sequence(start)
|
|
1058
|
+
def escape_sequence(start, mode: :double, quote: nil, delimiters: [])
|
|
906
1059
|
advance
|
|
907
1060
|
fail!("unterminated escape", start) if eof?
|
|
908
1061
|
value = byte
|
|
909
|
-
|
|
1062
|
+
escaped = escaped_character
|
|
1063
|
+
case mode
|
|
1064
|
+
when :single
|
|
1065
|
+
return escaped if value == 92 || value == quote
|
|
1066
|
+
"\\" + escaped
|
|
1067
|
+
when :percent_q, :word, :regexp
|
|
1068
|
+
if mode == :regexp
|
|
1069
|
+
return escaped if delimiters.include?(value) && delimiters.uniq.length == 1
|
|
1070
|
+
return "\\" + escaped
|
|
1071
|
+
end
|
|
1072
|
+
return escaped if value == 92 || delimiters.include?(value)
|
|
1073
|
+
if mode == :word && whitespace_byte?(value)
|
|
1074
|
+
return escaped
|
|
1075
|
+
end
|
|
1076
|
+
"\\" + escaped
|
|
1077
|
+
when :double
|
|
1078
|
+
double_quoted_escape(value, escaped, start, quote: quote, delimiters: delimiters)
|
|
1079
|
+
else
|
|
1080
|
+
fail!("unsupported escape mode", start)
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
def double_quoted_escape(value, escaped, start, quote:, delimiters:)
|
|
1085
|
+
return "" if value == 10
|
|
1086
|
+
if value == 13
|
|
1087
|
+
advance if byte == 10
|
|
1088
|
+
return ""
|
|
1089
|
+
end
|
|
910
1090
|
return "\n" if value == 110
|
|
911
1091
|
return "\t" if value == 116
|
|
912
1092
|
return "\r" if value == 114
|
|
913
1093
|
return "\f" if value == 102
|
|
914
1094
|
return "\a" if value == 97
|
|
915
1095
|
return "\e" if value == 101
|
|
916
|
-
return
|
|
917
|
-
value
|
|
1096
|
+
return "\b" if value == 98
|
|
1097
|
+
return " " if value == 115
|
|
1098
|
+
return "\v" if value == 118
|
|
1099
|
+
return escaped if value == 92 || value == quote || delimiters.include?(value) || value == 34 || value == 39 || value == 96
|
|
1100
|
+
return hexadecimal_escape(start) if value == 120
|
|
1101
|
+
return octal_escape(value) if value.between?(48, 55)
|
|
1102
|
+
return unicode_escape(start) if value == 117
|
|
1103
|
+
return control_escape(start) if value == 99
|
|
1104
|
+
return control_escape(start, dash: true) if value == 67
|
|
1105
|
+
return meta_escape(start) if value == 77
|
|
1106
|
+
escaped
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def hexadecimal_escape(start)
|
|
1110
|
+
digits = +""
|
|
1111
|
+
2.times do
|
|
1112
|
+
break unless hex_digit?(byte)
|
|
1113
|
+
digits << byte.chr
|
|
1114
|
+
advance
|
|
1115
|
+
end
|
|
1116
|
+
fail!("invalid hexadecimal escape", start) if digits.empty?
|
|
1117
|
+
raw_byte(digits.to_i(16))
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
def octal_escape(first)
|
|
1121
|
+
digits = first.chr
|
|
1122
|
+
2.times do
|
|
1123
|
+
break unless byte && byte.between?(48, 55)
|
|
1124
|
+
digits << byte.chr
|
|
1125
|
+
advance
|
|
1126
|
+
end
|
|
1127
|
+
raw_byte(digits.to_i(8) & 0xff)
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
def unicode_escape(start)
|
|
1131
|
+
codepoints = []
|
|
1132
|
+
if byte == 123
|
|
1133
|
+
advance
|
|
1134
|
+
loop do
|
|
1135
|
+
advance while whitespace_byte?(byte)
|
|
1136
|
+
digits = +""
|
|
1137
|
+
while hex_digit?(byte)
|
|
1138
|
+
digits << byte.chr
|
|
1139
|
+
advance
|
|
1140
|
+
end
|
|
1141
|
+
fail!("invalid Unicode escape", start) if digits.empty? || digits.length > 6
|
|
1142
|
+
codepoints << unicode_codepoint(digits.to_i(16), start)
|
|
1143
|
+
advance while whitespace_byte?(byte)
|
|
1144
|
+
break if byte == 125
|
|
1145
|
+
fail!("invalid Unicode escape", start) unless byte
|
|
1146
|
+
end
|
|
1147
|
+
advance
|
|
1148
|
+
else
|
|
1149
|
+
4.times do
|
|
1150
|
+
fail!("invalid Unicode escape", start) unless hex_digit?(byte)
|
|
1151
|
+
codepoints << (byte.chr)
|
|
1152
|
+
advance
|
|
1153
|
+
end
|
|
1154
|
+
codepoints = [unicode_codepoint(codepoints.join.to_i(16), start)]
|
|
1155
|
+
end
|
|
1156
|
+
codepoints.join
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
def unicode_codepoint(value, start)
|
|
1160
|
+
fail!("invalid Unicode codepoint", start) if value > 0x10ffff || value.between?(0xd800, 0xdfff)
|
|
1161
|
+
value.chr(Encoding::UTF_8)
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
def control_escape(start, dash: false)
|
|
1165
|
+
if dash
|
|
1166
|
+
fail!("invalid control escape", start) unless byte == 45
|
|
1167
|
+
advance
|
|
1168
|
+
end
|
|
1169
|
+
fail!("unsupported compound control escape", start) if byte == 92
|
|
1170
|
+
character = source_character
|
|
1171
|
+
fail!("invalid control escape", start) unless character.bytesize == 1 && character.getbyte(0) < 128
|
|
1172
|
+
value = character.getbyte(0)
|
|
1173
|
+
raw_byte(value == 63 ? 127 : value & 0x9f)
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
def meta_escape(start)
|
|
1177
|
+
fail!("invalid meta escape", start) unless byte == 45
|
|
1178
|
+
advance
|
|
1179
|
+
fail!("unsupported compound meta escape", start) if byte == 92
|
|
1180
|
+
character = source_character
|
|
1181
|
+
fail!("invalid meta escape", start) unless character.bytesize == 1 && character.getbyte(0) < 128
|
|
1182
|
+
raw_byte(character.getbyte(0) | 0x80)
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
def raw_byte(value)
|
|
1186
|
+
encoding = @source_encoding == Encoding::US_ASCII.name && value >= 0x80 ?
|
|
1187
|
+
Encoding::BINARY : Encoding::UTF_8
|
|
1188
|
+
[value].pack("C").force_encoding(encoding)
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
def hex_digit?(value)
|
|
1192
|
+
value && (value.between?(48, 57) || value.between?(65, 70) || value.between?(97, 102))
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def whitespace_byte?(value)
|
|
1196
|
+
[9, 10, 11, 12, 13, 32].include?(value)
|
|
918
1197
|
end
|
|
919
1198
|
|
|
920
1199
|
def regexp_or_operator(start)
|
|
921
|
-
return operator_or_punctuation(start) if [".", :tCOLON2, :tANDDOT, :keyword_def, :tSYMBEG].include?(@previous)
|
|
922
|
-
|
|
1200
|
+
return operator_or_punctuation(start) if [".", :tCOLON2, :tANDDOT, :keyword_def, :tSYMBEG].include?(@previous) ||
|
|
1201
|
+
super_binary_operator?(start, 1)
|
|
1202
|
+
if @context.begin_expression || regexp_command_argument?
|
|
923
1203
|
advance
|
|
924
|
-
value = read_regexp(start)
|
|
1204
|
+
value, metadata = read_regexp(start)
|
|
925
1205
|
content = value.is_a?(Array) ? value : [[:tSTRING_CONTENT, value]]
|
|
926
|
-
@pending = content + [[:tREGEXP_END,
|
|
1206
|
+
@pending = content + [[:tREGEXP_END, metadata]]
|
|
927
1207
|
return [:tREGEXP_BEG, nil]
|
|
928
1208
|
end
|
|
929
1209
|
operator_or_punctuation(start)
|
|
@@ -937,9 +1217,9 @@ class Lexer
|
|
|
937
1217
|
until eof?
|
|
938
1218
|
value = byte
|
|
939
1219
|
if value == 92
|
|
940
|
-
result << value.chr
|
|
941
1220
|
advance
|
|
942
1221
|
fail!("unterminated regexp", start) if eof?
|
|
1222
|
+
result << "\\" unless byte == 47
|
|
943
1223
|
result << byte.chr
|
|
944
1224
|
advance
|
|
945
1225
|
elsif value == 91
|
|
@@ -950,22 +1230,38 @@ class Lexer
|
|
|
950
1230
|
in_class = false
|
|
951
1231
|
result << value.chr
|
|
952
1232
|
advance
|
|
953
|
-
elsif value == 35 && byte(1) == 123
|
|
954
|
-
tokens << [:tSTRING_CONTENT, result] unless result.empty?
|
|
1233
|
+
elsif value == 35 && byte(1) == 123
|
|
1234
|
+
tokens << [:tSTRING_CONTENT, result.dup.force_encoding(@source_encoding)] unless result.empty?
|
|
955
1235
|
result = +""
|
|
956
1236
|
interpolated = true
|
|
957
1237
|
advance(2)
|
|
1238
|
+
expression_line = @line
|
|
958
1239
|
expression = read_interpolation_source(start)
|
|
959
|
-
inner = self.class.new(expression, filename: @filename).each.to_a
|
|
1240
|
+
inner = self.class.new(expression, filename: @filename, initial_line: expression_line).each.to_a
|
|
960
1241
|
inner.pop if inner.last == [0, nil]
|
|
1242
|
+
inner.map! { |token| token.first == "\n" ? [";", nil] : token }
|
|
961
1243
|
tokens << [:tSTRING_DBEG, nil]
|
|
962
1244
|
tokens.concat(inner)
|
|
963
1245
|
tokens << [:tSTRING_DEND, nil]
|
|
1246
|
+
elsif value == 35 && (length = shorthand_interpolation_length)
|
|
1247
|
+
tokens << [:tSTRING_CONTENT, result.dup.force_encoding(@source_encoding)] unless result.empty?
|
|
1248
|
+
result = +""
|
|
1249
|
+
interpolated = true
|
|
1250
|
+
tokens.concat(shorthand_interpolation_tokens(start, length))
|
|
964
1251
|
elsif value == 47 && !in_class
|
|
965
1252
|
advance
|
|
966
|
-
|
|
1253
|
+
metadata = read_regexp_options
|
|
1254
|
+
result.scan(/\(\?<([A-Za-z_][A-Za-z0-9_]*)>/) do
|
|
1255
|
+
@local_variables[Regexp.last_match(1).to_sym] = true
|
|
1256
|
+
end
|
|
1257
|
+
result.force_encoding(@source_encoding)
|
|
967
1258
|
tokens << [:tSTRING_CONTENT, result] unless result.empty?
|
|
968
|
-
|
|
1259
|
+
if metadata[:encoding] == "US-ASCII"
|
|
1260
|
+
tokens.each do |token|
|
|
1261
|
+
token[1].force_encoding(Encoding::BINARY) if token[0] == :tSTRING_CONTENT
|
|
1262
|
+
end
|
|
1263
|
+
end
|
|
1264
|
+
return [interpolated ? tokens : result, metadata]
|
|
969
1265
|
else
|
|
970
1266
|
result << value.chr
|
|
971
1267
|
advance
|
|
@@ -974,6 +1270,27 @@ class Lexer
|
|
|
974
1270
|
fail!("unterminated regexp", start)
|
|
975
1271
|
end
|
|
976
1272
|
|
|
1273
|
+
def read_regexp_options
|
|
1274
|
+
modifiers = +""
|
|
1275
|
+
while byte && byte.between?(97, 122)
|
|
1276
|
+
modifiers << byte.chr
|
|
1277
|
+
advance
|
|
1278
|
+
end
|
|
1279
|
+
options = 0
|
|
1280
|
+
options |= 1 if modifiers.include?("i")
|
|
1281
|
+
options |= 2 if modifiers.include?("x")
|
|
1282
|
+
options |= 4 if modifiers.include?("m")
|
|
1283
|
+
encoding = case modifiers.chars.find { |modifier| ["n", "e", "s", "u"].include?(modifier) }
|
|
1284
|
+
when "n" then "US-ASCII"
|
|
1285
|
+
when "e" then "EUC-JP"
|
|
1286
|
+
when "s" then "Windows-31J"
|
|
1287
|
+
when "u" then "UTF-8"
|
|
1288
|
+
end
|
|
1289
|
+
options |= encoding == "US-ASCII" ? 32 : 16 if encoding
|
|
1290
|
+
{ options: options, encoding: encoding, source_encoding: @source_encoding,
|
|
1291
|
+
once: modifiers.include?("o") }.freeze
|
|
1292
|
+
end
|
|
1293
|
+
|
|
977
1294
|
def percent_token(start)
|
|
978
1295
|
return operator_or_punctuation(start) if [".", :tCOLON2, :tANDDOT].include?(@previous)
|
|
979
1296
|
literal_kind = [113, 81, 119, 87, 105, 73, 114, 115, 120, 88].include?(byte(1))
|
|
@@ -996,42 +1313,92 @@ class Lexer
|
|
|
996
1313
|
advance
|
|
997
1314
|
closing = { 40 => 41, 91 => 93, 123 => 125, 60 => 62 }.fetch(delimiter, delimiter)
|
|
998
1315
|
interpolate = [81, 87, 73, 114, 120, 88].include?(kind)
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1316
|
+
word_literal = [119, 87, 105, 73].include?(kind)
|
|
1317
|
+
content = if word_literal
|
|
1318
|
+
read_word_delimited(closing, delimiter, start, interpolate: [87, 73].include?(kind), symbols: [105, 73].include?(kind))
|
|
1319
|
+
elsif interpolate
|
|
1320
|
+
mode = kind == 114 ? :regexp : :double
|
|
1321
|
+
read_interpolated_delimited(closing, start, mode: mode, delimiters: [closing, delimiter])
|
|
1322
|
+
else
|
|
1323
|
+
read_delimited(closing, start, mode: :percent_q, delimiters: [closing, delimiter])
|
|
1324
|
+
end
|
|
1325
|
+
regexp_metadata = read_regexp_options if kind == 114
|
|
1326
|
+
token = { 113 => :tSTRING_BEG, 81 => :tSTRING_BEG, 87 => :tWORDS_BEG, 119 => :tWORDS_BEG,
|
|
1327
|
+
73 => :tSYMBOLS_BEG, 105 => :tSYMBOLS_BEG, 114 => :tREGEXP_BEG,
|
|
1005
1328
|
115 => :tSYMBEG, 120 => :tXSTRING_BEG, 88 => :tXSTRING_BEG }.fetch(kind)
|
|
1006
1329
|
terminator = kind == 114 ? :tREGEXP_END : :tSTRING_END
|
|
1007
|
-
@pending = if
|
|
1008
|
-
content = content.map { |token, value| value.to_s }.join if content.is_a?(Array)
|
|
1009
|
-
word_tokens(content, symbols: [105, 73].include?(kind)) + [[terminator, nil]]
|
|
1010
|
-
elsif content.is_a?(Array)
|
|
1330
|
+
@pending = if word_literal
|
|
1011
1331
|
content + [[terminator, nil]]
|
|
1332
|
+
elsif content.is_a?(Array)
|
|
1333
|
+
content + [[terminator, regexp_metadata]]
|
|
1012
1334
|
else
|
|
1013
|
-
[[:tSTRING_CONTENT, content], [terminator,
|
|
1335
|
+
[[:tSTRING_CONTENT, content], [terminator, regexp_metadata]]
|
|
1014
1336
|
end
|
|
1015
1337
|
[token, nil]
|
|
1016
1338
|
end
|
|
1017
1339
|
|
|
1018
|
-
def
|
|
1019
|
-
words = content.split(/[\t\n\f\r ]+/).reject(&:empty?)
|
|
1340
|
+
def read_word_delimited(closing, opening, start, interpolate:, symbols:)
|
|
1020
1341
|
tokens = [[" ", nil]]
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1342
|
+
content = +""
|
|
1343
|
+
word_started = false
|
|
1344
|
+
depth = 0
|
|
1345
|
+
paired_opening = { 41 => 40, 93 => 91, 125 => 123, 62 => 60 }.fetch(closing, -1)
|
|
1346
|
+
flush_content = lambda do
|
|
1347
|
+
unless content.empty?
|
|
1348
|
+
tokens << [:tSTRING_CONTENT, content]
|
|
1349
|
+
content = +""
|
|
1350
|
+
end
|
|
1024
1351
|
end
|
|
1025
|
-
|
|
1352
|
+
flush_word = lambda do
|
|
1353
|
+
flush_content.call
|
|
1354
|
+
if word_started
|
|
1355
|
+
tokens << [" ", nil]
|
|
1356
|
+
word_started = false
|
|
1357
|
+
end
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
until eof?
|
|
1361
|
+
value = byte
|
|
1362
|
+
if value == 92
|
|
1363
|
+
content << escape_sequence(start, mode: interpolate ? :double : :word,
|
|
1364
|
+
delimiters: [closing, opening])
|
|
1365
|
+
word_started = true
|
|
1366
|
+
elsif value == 35 && interpolate && byte(1) == 123
|
|
1367
|
+
flush_content.call
|
|
1368
|
+
word_started = true
|
|
1369
|
+
advance(2)
|
|
1370
|
+
expression_line = @line
|
|
1371
|
+
expression = read_interpolation_source(start)
|
|
1372
|
+
inner = self.class.new(expression, filename: @filename, initial_line: expression_line).each.to_a
|
|
1373
|
+
inner.pop if inner.last == [0, nil]
|
|
1374
|
+
inner.map! { |token| token.first == "\n" ? [";", nil] : token }
|
|
1375
|
+
tokens << [:tSTRING_DBEG, nil]
|
|
1376
|
+
tokens.concat(inner)
|
|
1377
|
+
tokens << [:tSTRING_DEND, nil]
|
|
1378
|
+
elsif value == closing && depth.zero?
|
|
1379
|
+
flush_word.call
|
|
1380
|
+
advance
|
|
1381
|
+
return tokens
|
|
1382
|
+
elsif whitespace_byte?(value)
|
|
1383
|
+
flush_word.call
|
|
1384
|
+
advance
|
|
1385
|
+
else
|
|
1386
|
+
depth += 1 if value == paired_opening
|
|
1387
|
+
depth -= 1 if value == closing && depth.positive?
|
|
1388
|
+
append_source_character(content)
|
|
1389
|
+
word_started = true
|
|
1390
|
+
end
|
|
1391
|
+
end
|
|
1392
|
+
fail!("unterminated percent literal", start)
|
|
1026
1393
|
end
|
|
1027
1394
|
|
|
1028
|
-
def read_delimited(closing, start, interpolate: false)
|
|
1395
|
+
def read_delimited(closing, start, mode:, delimiters: [], interpolate: false)
|
|
1029
1396
|
result = +""
|
|
1030
1397
|
depth = 0
|
|
1031
1398
|
until eof?
|
|
1032
1399
|
value = byte
|
|
1033
1400
|
if value == 92
|
|
1034
|
-
result << escape_sequence(start)
|
|
1401
|
+
result << escape_sequence(start, mode: mode, delimiters: delimiters)
|
|
1035
1402
|
elsif value == 35 && interpolate && byte(1) == 123
|
|
1036
1403
|
fail!("string interpolation cannot be represented by the AST", start)
|
|
1037
1404
|
elsif value == closing && depth.zero?
|
|
@@ -1040,14 +1407,13 @@ class Lexer
|
|
|
1040
1407
|
else
|
|
1041
1408
|
depth += 1 if value == ({ 41 => 40, 93 => 91, 125 => 123, 62 => 60 }.fetch(closing, -1))
|
|
1042
1409
|
depth -= 1 if value == closing && depth.positive?
|
|
1043
|
-
result
|
|
1044
|
-
advance
|
|
1410
|
+
append_source_character(result)
|
|
1045
1411
|
end
|
|
1046
1412
|
end
|
|
1047
1413
|
fail!("unterminated percent literal", start)
|
|
1048
1414
|
end
|
|
1049
1415
|
|
|
1050
|
-
def read_interpolated_delimited(closing, start)
|
|
1416
|
+
def read_interpolated_delimited(closing, start, mode:, delimiters: [])
|
|
1051
1417
|
result = +""
|
|
1052
1418
|
tokens = []
|
|
1053
1419
|
depth = 0
|
|
@@ -1055,7 +1421,7 @@ class Lexer
|
|
|
1055
1421
|
until eof?
|
|
1056
1422
|
value = byte
|
|
1057
1423
|
if value == 92
|
|
1058
|
-
result << escape_sequence(start)
|
|
1424
|
+
result << escape_sequence(start, mode: mode, delimiters: delimiters)
|
|
1059
1425
|
elsif value == 35 && byte(1) == 123
|
|
1060
1426
|
tokens << [:tSTRING_CONTENT, result] unless result.empty?
|
|
1061
1427
|
result = +""
|
|
@@ -1063,56 +1429,114 @@ class Lexer
|
|
|
1063
1429
|
expression = read_interpolation_source(start)
|
|
1064
1430
|
inner = self.class.new(expression, filename: @filename).each.to_a
|
|
1065
1431
|
inner.pop if inner.last == [0, nil]
|
|
1432
|
+
inner.map! { |token| token.first == "\n" ? [";", nil] : token }
|
|
1066
1433
|
tokens << [:tSTRING_DBEG, nil]
|
|
1067
1434
|
tokens.concat(inner)
|
|
1068
1435
|
tokens << [:tSTRING_DEND, nil]
|
|
1069
1436
|
elsif value == closing && depth.zero?
|
|
1070
1437
|
advance
|
|
1071
|
-
tokens << [:tSTRING_CONTENT, result]
|
|
1438
|
+
tokens << [:tSTRING_CONTENT, result] if tokens.empty? || !result.empty?
|
|
1072
1439
|
return tokens
|
|
1073
1440
|
else
|
|
1074
1441
|
depth += 1 if value == opening
|
|
1075
1442
|
depth -= 1 if value == closing && depth.positive?
|
|
1076
|
-
result
|
|
1077
|
-
advance
|
|
1443
|
+
append_source_character(result)
|
|
1078
1444
|
end
|
|
1079
1445
|
end
|
|
1080
1446
|
fail!("unterminated percent literal", start)
|
|
1081
1447
|
end
|
|
1082
1448
|
|
|
1083
1449
|
def interpolated_content_tokens(source, start)
|
|
1084
|
-
|
|
1450
|
+
lexer = self.class.new(source, filename: @filename)
|
|
1451
|
+
lexer.instance_variable_set(:@source_encoding, @source_encoding)
|
|
1452
|
+
lexer.send(:read_interpolated_body, start)
|
|
1453
|
+
end
|
|
1454
|
+
|
|
1455
|
+
def read_interpolated_body(start)
|
|
1085
1456
|
tokens = []
|
|
1086
|
-
literal =
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if
|
|
1457
|
+
literal = source_string
|
|
1458
|
+
until eof?
|
|
1459
|
+
value = byte
|
|
1460
|
+
if value == 92
|
|
1461
|
+
literal << escape_sequence(start, mode: :double)
|
|
1462
|
+
elsif value == 35 && byte(1) == 123
|
|
1090
1463
|
tokens << [:tSTRING_CONTENT, literal] unless literal.empty?
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
index = expr_start
|
|
1094
|
-
while index < source.bytesize && depth.positive?
|
|
1095
|
-
depth += 1 if source.getbyte(index) == 123
|
|
1096
|
-
depth -= 1 if source.getbyte(index) == 125
|
|
1097
|
-
index += 1
|
|
1098
|
-
end
|
|
1099
|
-
fail!("unterminated string interpolation", start) unless depth.zero?
|
|
1100
|
-
expression = source.byteslice(expr_start, index - expr_start - 1)
|
|
1464
|
+
advance(2)
|
|
1465
|
+
expression = read_interpolation_source(start)
|
|
1101
1466
|
inner = self.class.new(expression, filename: @filename).each.to_a
|
|
1102
1467
|
inner.pop if inner.last == [0, nil]
|
|
1468
|
+
inner.map! { |token| token.first == "\n" ? [";", nil] : token }
|
|
1103
1469
|
tokens << [:tSTRING_DBEG, nil]
|
|
1104
1470
|
tokens.concat(inner)
|
|
1105
1471
|
tokens << [:tSTRING_DEND, nil]
|
|
1106
|
-
literal =
|
|
1472
|
+
literal = source_string
|
|
1473
|
+
elsif value == 35 && (length = shorthand_interpolation_length)
|
|
1474
|
+
tokens << [:tSTRING_CONTENT, literal] unless literal.empty?
|
|
1475
|
+
literal = source_string
|
|
1476
|
+
tokens.concat(shorthand_interpolation_tokens(start, length))
|
|
1107
1477
|
else
|
|
1108
|
-
literal
|
|
1109
|
-
index += 1
|
|
1478
|
+
append_source_character(literal)
|
|
1110
1479
|
end
|
|
1111
1480
|
end
|
|
1112
1481
|
tokens << [:tSTRING_CONTENT, literal] unless literal.empty?
|
|
1113
1482
|
tokens
|
|
1114
1483
|
end
|
|
1115
1484
|
|
|
1485
|
+
def shorthand_interpolation_tokens(start, length)
|
|
1486
|
+
expression_line = @line
|
|
1487
|
+
expression = @source.byteslice(@index + 1, length).force_encoding(Encoding::UTF_8)
|
|
1488
|
+
advance(length + 1)
|
|
1489
|
+
inner = self.class.new(expression, filename: @filename, initial_line: expression_line).each.to_a
|
|
1490
|
+
inner.pop if inner.last == [0, nil]
|
|
1491
|
+
[[:tSTRING_DBEG, nil], *inner, [:tSTRING_DEND, nil]]
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
def shorthand_interpolation_length
|
|
1495
|
+
return unless byte == 35
|
|
1496
|
+
|
|
1497
|
+
case byte(1)
|
|
1498
|
+
when 64
|
|
1499
|
+
offset = 2
|
|
1500
|
+
offset += 1 if byte(offset) == 64
|
|
1501
|
+
return unless identifier_start_byte?(byte(offset))
|
|
1502
|
+
offset += 1 while identifier_byte?(byte(offset))
|
|
1503
|
+
offset - 1
|
|
1504
|
+
when 36
|
|
1505
|
+
offset = 2
|
|
1506
|
+
if byte(offset) == 45
|
|
1507
|
+
return unless identifier_start_byte?(byte(offset + 1))
|
|
1508
|
+
offset += 2
|
|
1509
|
+
offset += 1 while identifier_byte?(byte(offset))
|
|
1510
|
+
elsif digit_byte?(byte(offset), 10)
|
|
1511
|
+
offset += 1
|
|
1512
|
+
offset += 1 while digit_byte?(byte(offset), 10)
|
|
1513
|
+
elsif identifier_start_byte?(byte(offset))
|
|
1514
|
+
offset += 1
|
|
1515
|
+
offset += 1 while identifier_byte?(byte(offset))
|
|
1516
|
+
elsif [33, 36, 38, 39, 42, 43, 44, 45, 46, 47, 58, 59, 60, 61, 62, 63, 64, 92, 96, 126].include?(byte(offset))
|
|
1517
|
+
offset += 1
|
|
1518
|
+
else
|
|
1519
|
+
return
|
|
1520
|
+
end
|
|
1521
|
+
offset - 1
|
|
1522
|
+
end
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
def identifier_start_byte?(value)
|
|
1526
|
+
value && (value == 95 || value.between?(65, 90) || value.between?(97, 122) || value >= 128)
|
|
1527
|
+
end
|
|
1528
|
+
|
|
1529
|
+
def source_local_variable_names
|
|
1530
|
+
@source.scan(/(?<![.@:$])([a-z_][a-zA-Z0-9_]*)[ \t\r\n]*[+\-*\/%]?=(?!=)/).flatten.map(&:to_sym).uniq
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
def end_data_marker?(start)
|
|
1534
|
+
line_start = @source.rindex("\n".b, start - 1)
|
|
1535
|
+
line_start = line_start ? line_start + 1 : 0
|
|
1536
|
+
prefix = @source.byteslice(line_start, start - line_start)
|
|
1537
|
+
prefix.bytes.all? { |value| [9, 13, 32].include?(value) }
|
|
1538
|
+
end
|
|
1539
|
+
|
|
1116
1540
|
def heredoc_or_operator(start)
|
|
1117
1541
|
return operator_or_punctuation(start) if [".", :keyword_class].include?(@previous)
|
|
1118
1542
|
if byte(1) == 60 && heredoc_prefix?
|
|
@@ -1137,6 +1561,7 @@ class Lexer
|
|
|
1137
1561
|
end
|
|
1138
1562
|
|
|
1139
1563
|
def heredoc_token(start)
|
|
1564
|
+
suffix_line = @line
|
|
1140
1565
|
advance(2)
|
|
1141
1566
|
indent = byte == 45 || byte == 126
|
|
1142
1567
|
squiggly = byte == 126
|
|
@@ -1165,11 +1590,14 @@ class Lexer
|
|
|
1165
1590
|
@pending = content_tokens + [[:tSTRING_END, nil]]
|
|
1166
1591
|
prefix, headers, suffix = extract_heredoc_headers(suffix)
|
|
1167
1592
|
unless prefix.empty?
|
|
1168
|
-
prefix_tokens = heredoc_suffix_tokens(prefix)
|
|
1593
|
+
prefix_tokens = heredoc_suffix_tokens(prefix, suffix_line)
|
|
1169
1594
|
@pending.concat(prefix_tokens)
|
|
1170
1595
|
end
|
|
1171
1596
|
@heredoc_queue.concat(headers)
|
|
1172
|
-
|
|
1597
|
+
unless suffix.empty?
|
|
1598
|
+
@deferred_suffix = suffix
|
|
1599
|
+
@deferred_suffix_line = suffix_line
|
|
1600
|
+
end
|
|
1173
1601
|
@heredoc_newline_pending = true
|
|
1174
1602
|
[quote == 96 ? :tXSTRING_BEG : :tSTRING_BEG, nil]
|
|
1175
1603
|
end
|
|
@@ -1190,7 +1618,7 @@ class Lexer
|
|
|
1190
1618
|
advance if byte == 10
|
|
1191
1619
|
fail!("unterminated heredoc", start) if eof?
|
|
1192
1620
|
end
|
|
1193
|
-
body = body.force_encoding(
|
|
1621
|
+
body = body.force_encoding(@source_encoding)
|
|
1194
1622
|
squiggly ? dedent_heredoc(body) : body
|
|
1195
1623
|
end
|
|
1196
1624
|
|
|
@@ -1207,8 +1635,9 @@ class Lexer
|
|
|
1207
1635
|
[suffix[0...match.begin(0)], headers, trailing]
|
|
1208
1636
|
end
|
|
1209
1637
|
|
|
1210
|
-
def heredoc_suffix_tokens(source)
|
|
1638
|
+
def heredoc_suffix_tokens(source, line = @line)
|
|
1211
1639
|
suffix_lexer = self.class.new(source, filename: @filename)
|
|
1640
|
+
suffix_lexer.instance_variable_set(:@line, line)
|
|
1212
1641
|
suffix_lexer.context.begin_expression = false
|
|
1213
1642
|
suffix_lexer.context.lex_state = :expr_end
|
|
1214
1643
|
suffix_lexer.instance_variable_set(:@previous, :tSTRING_END)
|
|
@@ -1249,12 +1678,12 @@ class Lexer
|
|
|
1249
1678
|
if @previous == :keyword_def && ["-@", "+@"].include?(@source.byteslice(@index, 2))
|
|
1250
1679
|
value = @source.byteslice(@index, 2)
|
|
1251
1680
|
advance(2)
|
|
1252
|
-
return [value == "-@" ? :tUMINUS : :tUPLUS,
|
|
1681
|
+
return [value == "-@" ? :tUMINUS : :tUPLUS, value]
|
|
1253
1682
|
end
|
|
1254
1683
|
if (@previous == :tSYMBEG || @context.alias_context) && ["-@", "+@"].include?(@source.byteslice(@index, 2))
|
|
1255
1684
|
value = @source.byteslice(@index, 2)
|
|
1256
1685
|
advance(2)
|
|
1257
|
-
return [value == "-@" ? :tUMINUS : :tUPLUS,
|
|
1686
|
+
return [value == "-@" ? :tUMINUS : :tUPLUS, value]
|
|
1258
1687
|
end
|
|
1259
1688
|
text = OPERATORS.sort_by { |operator| -operator.bytesize }.find do |operator|
|
|
1260
1689
|
next false if ["[]", "[]="].include?(operator) && start.positive? &&
|
|
@@ -1271,7 +1700,7 @@ class Lexer
|
|
|
1271
1700
|
@previous_previous == :keyword_alias
|
|
1272
1701
|
if text && !(begin_expression? && !operator_method && ["[]", "[]="].include?(text))
|
|
1273
1702
|
advance(text.bytesize)
|
|
1274
|
-
token = if text == "**" && @context.begin_expression
|
|
1703
|
+
token = if text == "**" && @context.begin_expression && !super_binary_operator?(start, text.bytesize)
|
|
1275
1704
|
:tDSTAR
|
|
1276
1705
|
elsif text == ".." && @context.begin_expression
|
|
1277
1706
|
:tBDOT2
|
|
@@ -1344,11 +1773,18 @@ class Lexer
|
|
|
1344
1773
|
@context.method_body_start = true
|
|
1345
1774
|
@context.method_header = false
|
|
1346
1775
|
end
|
|
1347
|
-
elsif value == "-" && @
|
|
1348
|
-
return [:tUMINUS,
|
|
1349
|
-
elsif value == "+" && @
|
|
1350
|
-
return [:tUPLUS,
|
|
1351
|
-
elsif value == "
|
|
1776
|
+
elsif value == "-" && @previous == :tSYMBEG
|
|
1777
|
+
return [:tUMINUS, :-]
|
|
1778
|
+
elsif value == "+" && @previous == :tSYMBEG
|
|
1779
|
+
return [:tUPLUS, :+]
|
|
1780
|
+
elsif value == "-" && (@context.begin_expression || unicode_identifier_command_argument?(start) || unary_argument_without_space?(start)) && !super_binary_operator?(start, 1)
|
|
1781
|
+
token = numeric_literal_follows? ? :tUMINUS_NUM : :tUMINUS
|
|
1782
|
+
return [token, @previous == :keyword_def || @context.alias_context ? value : nil]
|
|
1783
|
+
elsif value == "+" && (@context.begin_expression || unicode_identifier_command_argument?(start) || unary_argument_without_space?(start)) && !super_binary_operator?(start, 1)
|
|
1784
|
+
return [:tUPLUS, @previous == :keyword_def || @context.alias_context ? value : nil]
|
|
1785
|
+
elsif value == "*" && @context.begin_expression && !super_binary_operator?(start, 1)
|
|
1786
|
+
return [:tSTAR, nil]
|
|
1787
|
+
elsif value == "*" && splat_argument_without_space?(start)
|
|
1352
1788
|
return [:tSTAR, nil]
|
|
1353
1789
|
elsif value == "&" && @previous == :tSYMBEG
|
|
1354
1790
|
return [value, nil]
|
|
@@ -1366,6 +1802,47 @@ class Lexer
|
|
|
1366
1802
|
fail!("invalid byte", start)
|
|
1367
1803
|
end
|
|
1368
1804
|
|
|
1805
|
+
def super_binary_operator?(start, length)
|
|
1806
|
+
return false unless @previous == :keyword_super
|
|
1807
|
+
|
|
1808
|
+
before = start.positive? ? @source.getbyte(start - 1) : nil
|
|
1809
|
+
after = @source.getbyte(start + length)
|
|
1810
|
+
!whitespace_byte?(before) || whitespace_byte?(after)
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
def regexp_command_argument?
|
|
1814
|
+
@previous == :tIDENTIFIER && @previous_previous == :modifier_if &&
|
|
1815
|
+
!@local_variables.key?(@previous_value) &&
|
|
1816
|
+
!whitespace_byte?(byte(1))
|
|
1817
|
+
end
|
|
1818
|
+
|
|
1819
|
+
def unicode_identifier_command_argument?(start)
|
|
1820
|
+
return false unless @previous == :tIDENTIFIER
|
|
1821
|
+
return false unless @previous_value&.to_s&.match?(Regexp.new("\\p{Cf}"))
|
|
1822
|
+
|
|
1823
|
+
before = start.positive? ? @source.getbyte(start - 1) : nil
|
|
1824
|
+
after = @source.getbyte(start + 1)
|
|
1825
|
+
whitespace_byte?(before) && !whitespace_byte?(after)
|
|
1826
|
+
end
|
|
1827
|
+
|
|
1828
|
+
def numeric_literal_follows?
|
|
1829
|
+
value = byte
|
|
1830
|
+
value && value.between?(48, 57)
|
|
1831
|
+
end
|
|
1832
|
+
|
|
1833
|
+
def splat_argument_without_space?(start)
|
|
1834
|
+
return false unless whitespace_byte?(@source.getbyte(start - 1))
|
|
1835
|
+
return false if whitespace_byte?(@source.getbyte(start + 1))
|
|
1836
|
+
return false if @previous == :tIDENTIFIER &&
|
|
1837
|
+
(@local_variables.key?(@previous_value) || @source_local_variables.include?(@previous_value))
|
|
1838
|
+
[:tIDENTIFIER, :tCONSTANT, :tFID].include?(@previous)
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1841
|
+
def unary_argument_without_space?(start)
|
|
1842
|
+
return false if whitespace_byte?(@source.getbyte(start + 1))
|
|
1843
|
+
splat_argument_without_space?(start)
|
|
1844
|
+
end
|
|
1845
|
+
|
|
1369
1846
|
def begin_expression?
|
|
1370
1847
|
@context.begin_expression
|
|
1371
1848
|
end
|