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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -2
  3. data/README.md +104 -7
  4. data/Rakefile +15 -1
  5. data/docs/architecture.dot +30 -0
  6. data/docs/architecture.md +96 -0
  7. data/docs/architecture.webp +0 -0
  8. data/docs/contributes.md +115 -0
  9. data/docs/usage.md +497 -0
  10. data/lib/arpaka/cli.rb +5 -53
  11. data/lib/arpaka/plugins/cli.rb +61 -0
  12. data/lib/arpaka/plugins/sample.rb +14 -0
  13. data/lib/arpaka/plugins/sample_runner.rb +21 -0
  14. data/lib/arpaka/{actions.rb → ruby/actions.rb} +230 -151
  15. data/lib/arpaka/ruby/class_name_parser.rb +97 -0
  16. data/lib/arpaka/ruby/cli.rb +64 -0
  17. data/lib/arpaka/ruby/frontend_emitter.rb +59 -0
  18. data/lib/arpaka/ruby/generator.rb +41 -0
  19. data/lib/arpaka/{ruby_grammar.rb → ruby/grammar.rb} +5 -56
  20. data/lib/arpaka/ruby/grammar_emitter.rb +71 -0
  21. data/lib/arpaka/ruby/profile.rb +129 -0
  22. data/lib/arpaka/{profile → ruby/profiles}/ruby_3_3.rb +21 -21
  23. data/lib/arpaka/{profile → ruby/profiles}/ruby_3_4.rb +21 -21
  24. data/lib/arpaka/{runtime → ruby/runtime/detailed}/ast.rb.erb +41 -5
  25. data/lib/arpaka/ruby/runtime/detailed/builder.rb.erb +1177 -0
  26. data/lib/arpaka/{runtime → ruby/runtime}/frontend.rb.erb +17 -4
  27. data/lib/arpaka/{runtime → ruby/runtime}/lexer.rb.erb +576 -99
  28. data/lib/arpaka/ruby/runtime/structured.rb.erb +111 -0
  29. data/lib/arpaka/ruby.rb +23 -0
  30. data/lib/arpaka.rb +6 -9
  31. data/lib/lrama/ruby/backend.rb +4 -9
  32. data/lib/lrama/ruby/parser.rb.erb +35 -9
  33. data/lib/lrama/ruby/parser_emitter.rb +41 -0
  34. data/lib/lrama/ruby/table_codec.rb +50 -0
  35. data/lib/lrama/ruby/version.rb +1 -1
  36. data/lib/lrama/ruby.rb +1 -0
  37. data/sig/arpaka.rbs +8 -3
  38. metadata +32 -16
  39. data/lib/arpaka/generator.rb +0 -51
  40. data/lib/arpaka/profile/ruby.rb +0 -49
  41. data/lib/arpaka/runtime/builder.rb.erb +0 -323
  42. /data/lib/arpaka/{BSDL → ruby/BSDL} +0 -0
  43. /data/lib/arpaka/{COPYING → ruby/COPYING} +0 -0
  44. /data/lib/arpaka/{source.json → ruby/source.json} +0 -0
  45. /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 = 1
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 byte == 61 && byte(1) != 61 &&
583
- ![".", :tCOLON2, :tANDDOT].include?(@previous)
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 no_argument_brace_block?
660
- return false if [".", :tCOLON2].include?(@previous_previous)
661
- return false if [:tLABEL, :tLSHFT, :tOROP, :tANDOP, :tASSOC, :tAMPER, "=", "(", ",", ":", "?"].include?(@previous_previous)
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
- [float ? :tFLOAT : :tINTEGER, float ? text.to_f : text.to_i]
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] unless content.empty?
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 << value.chr
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
- result << escape_sequence(start)
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 << value
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
- advance
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 value.chr if value == 92 || value == 34 || value == 39
917
- value.chr
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
- if @context.begin_expression
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, nil]]
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 && !in_class
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
- advance while byte && byte.between?(97, 122)
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
- return interpolated ? tokens : result
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
- content = interpolate ?
1000
- read_interpolated_delimited(closing, start) :
1001
- read_delimited(closing, start, interpolate: interpolate)
1002
- advance while kind == 114 && byte && byte.between?(97, 122)
1003
- token = { 113 => :tSTRING_BEG, 81 => :tSTRING_BEG, 119 => :tWORDS_BEG, 87 => :tQWORDS_BEG,
1004
- 105 => :tSYMBOLS_BEG, 73 => :tQSYMBOLS_BEG, 114 => :tREGEXP_BEG,
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 [119, 87, 105, 73].include?(kind)
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, nil]]
1335
+ [[:tSTRING_CONTENT, content], [terminator, regexp_metadata]]
1014
1336
  end
1015
1337
  [token, nil]
1016
1338
  end
1017
1339
 
1018
- def word_tokens(content, symbols:)
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
- words.each do |word|
1022
- tokens << [:tSTRING_CONTENT, symbols ? word.to_sym : word]
1023
- tokens << [" ", nil]
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
- tokens
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 << value.chr
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] unless result.empty?
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 << value.chr
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
- return [[:tSTRING_CONTENT, source]] unless source.include?("#" + "{")
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
- index = 0
1088
- while index < source.bytesize
1089
- if source.getbyte(index) == 35 && source.getbyte(index + 1) == 123
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
- depth = 1
1092
- expr_start = index + 2
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 << source.getbyte(index).chr
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
- @deferred_suffix = suffix unless suffix.empty?
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(Encoding::UTF_8)
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, nil]
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, nil]
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 == "-" && @context.begin_expression
1348
- return [:tUMINUS, nil]
1349
- elsif value == "+" && @context.begin_expression
1350
- return [:tUPLUS, nil]
1351
- elsif value == "*" && @context.begin_expression
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