peruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +35 -0
  3. data/CHANGELOG.md +15 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +51 -0
  6. data/Rakefile +92 -0
  7. data/bin/peruby +9 -0
  8. data/doc/COMPAT.md +54 -0
  9. data/doc/CONTRIBUTING.md +21 -0
  10. data/doc/DESIGN.md +25 -0
  11. data/doc/INCOMPATIBILITIES.md +30 -0
  12. data/doc/PERF.md +51 -0
  13. data/doc/ROADMAP.md +18 -0
  14. data/examples/hello.pl +1 -0
  15. data/examples/json.pl +2 -0
  16. data/examples/object.pl +5 -0
  17. data/examples/word_count.pl +6 -0
  18. data/lib/peruby/cli.rb +224 -0
  19. data/lib/peruby/compile_unit.rb +103 -0
  20. data/lib/peruby/compiler.rb +224 -0
  21. data/lib/peruby/errors.rb +40 -0
  22. data/lib/peruby/lexer/heredoc.rb +8 -0
  23. data/lib/peruby/lexer/keywords.rb +63 -0
  24. data/lib/peruby/lexer/number.rb +32 -0
  25. data/lib/peruby/lexer/quote_like.rb +72 -0
  26. data/lib/peruby/lexer/source_scanner.rb +104 -0
  27. data/lib/peruby/lexer/state.rb +46 -0
  28. data/lib/peruby/lexer/structure_scanner.rb +149 -0
  29. data/lib/peruby/lexer/term_scanner.rb +301 -0
  30. data/lib/peruby/lexer/token.rb +16 -0
  31. data/lib/peruby/lexer.rb +123 -0
  32. data/lib/peruby/node.rb +88 -0
  33. data/lib/peruby/op/assign.rb +128 -0
  34. data/lib/peruby/op/builtin.rb +903 -0
  35. data/lib/peruby/op/call.rb +378 -0
  36. data/lib/peruby/op/control.rb +256 -0
  37. data/lib/peruby/op/element.rb +136 -0
  38. data/lib/peruby/op/expression.rb +342 -0
  39. data/lib/peruby/op/io.rb +113 -0
  40. data/lib/peruby/op/list.rb +102 -0
  41. data/lib/peruby/op/literal.rb +84 -0
  42. data/lib/peruby/op/loop.rb +158 -0
  43. data/lib/peruby/op/regexp.rb +288 -0
  44. data/lib/peruby/op/variable.rb +534 -0
  45. data/lib/peruby/op.rb +47 -0
  46. data/lib/peruby/parser/grammar.rb +5797 -0
  47. data/lib/peruby/parser/grammar.y +576 -0
  48. data/lib/peruby/parser.rb +14 -0
  49. data/lib/peruby/runtime/code.rb +21 -0
  50. data/lib/peruby/runtime/conv.rb +140 -0
  51. data/lib/peruby/runtime/directory_handle.rb +18 -0
  52. data/lib/peruby/runtime/env.rb +129 -0
  53. data/lib/peruby/runtime/glob.rb +24 -0
  54. data/lib/peruby/runtime/interpolation.rb +223 -0
  55. data/lib/peruby/runtime/io_handle.rb +37 -0
  56. data/lib/peruby/runtime/local_stack.rb +90 -0
  57. data/lib/peruby/runtime/match_state.rb +62 -0
  58. data/lib/peruby/runtime/module_loader.rb +133 -0
  59. data/lib/peruby/runtime/mro.rb +94 -0
  60. data/lib/peruby/runtime/perl_array.rb +81 -0
  61. data/lib/peruby/runtime/perl_hash.rb +57 -0
  62. data/lib/peruby/runtime/ref.rb +43 -0
  63. data/lib/peruby/runtime/regexp_compiler.rb +75 -0
  64. data/lib/peruby/runtime/scalar.rb +27 -0
  65. data/lib/peruby/runtime/sprintf.rb +54 -0
  66. data/lib/peruby/runtime/stash.rb +50 -0
  67. data/lib/peruby/runtime/test_builder.rb +47 -0
  68. data/lib/peruby/runtime.rb +325 -0
  69. data/lib/peruby/validator.rb +236 -0
  70. data/lib/peruby/version.rb +5 -0
  71. data/lib/peruby.rb +31 -0
  72. data/t/00-basic.t +5 -0
  73. data/t/lib/MiniTest.pm +22 -0
  74. metadata +130 -0
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # Converts a Perl numeric literal to a Ruby value.
6
+ module Number
7
+ PATTERN = /\A(?:
8
+ v\d+(?:\.\d+)+ | 0[xX][0-9a-fA-F_]+ | 0[bB][01_]+ | 0[oO][0-7_]+ | 0[0-7_]+ |
9
+ (?:\d[\d_]*\.?[\d_]*|\.[\d_]+)(?:[eE][+-]?[\d_]+)?
10
+ )/x
11
+
12
+ module_function
13
+
14
+ def scan(source)
15
+ literal = PATTERN.match(source)&.[](0)
16
+ literal = literal.chop if literal&.end_with?('.') && source[literal.length] == '.'
17
+ [literal, convert(literal)] if literal
18
+ end
19
+
20
+ def convert(literal)
21
+ clean = literal.delete('_')
22
+ return clean.delete_prefix('v').split('.').map(&:to_i) if clean.start_with?('v')
23
+ return clean.to_i(16) if clean.match?(/\A0x/i)
24
+ return clean.to_i(2) if clean.match?(/\A0b/i)
25
+ return clean.to_i(8) if clean.match?(/\A0o/i) || clean.match?(/\A0[0-7]+\z/)
26
+ return clean.to_f if clean.match?(/[.e]/i)
27
+
28
+ clean.to_i
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # Raw quote-like payload. Interpolation is compiled in P7.
6
+ Quote = Struct.new(:operator, :parts, :modifiers, :interpolate, keyword_init: true)
7
+
8
+ # Scans q/qq/qw/qr/m/s/tr/y delimiters without interpreting their contents.
9
+ class QuoteLike
10
+ PAIRS = { '(' => ')', '[' => ']', '{' => '}', '<' => '>' }.freeze
11
+ TWO_PART = %i[s tr y].freeze
12
+
13
+ def initialize(source, index)
14
+ @source = source
15
+ @index = index
16
+ end
17
+
18
+ def scan(operator)
19
+ skip_space if TWO_PART.include?(operator) && paired_delimiter?
20
+ open = take
21
+ raise CompileError, "Missing delimiter for #{operator}" unless open
22
+
23
+ parts = [scan_part(open)]
24
+ parts << scan_second(open) if TWO_PART.include?(operator)
25
+ modifiers = take_while(/[a-z]/)
26
+ [Quote.new(operator:, parts:, modifiers:, interpolate: open != "'"), @index]
27
+ end
28
+
29
+ private
30
+
31
+ def scan_second(first_open)
32
+ skip_space if PAIRS.key?(first_open)
33
+ open = PAIRS.key?(first_open) ? take : first_open
34
+ scan_part(open)
35
+ end
36
+
37
+ def scan_part(open)
38
+ close = PAIRS.fetch(open, open)
39
+ depth = 1
40
+ output = +''
41
+ while (character = take)
42
+ return output if character == close && (depth -= 1).zero?
43
+
44
+ depth += 1 if character == open && open != close
45
+ output << character
46
+ output << take.to_s if character == '\\'
47
+ end
48
+ raise CompileError, "Can't find string terminator #{close.inspect} anywhere before EOF"
49
+ end
50
+
51
+ def paired_delimiter?
52
+ PAIRS.key?(@source[@index])
53
+ end
54
+
55
+ def skip_space
56
+ @index += 1 while @source[@index]&.match?(/\s/)
57
+ end
58
+
59
+ def take
60
+ character = @source[@index]
61
+ @index += 1 if character
62
+ character
63
+ end
64
+
65
+ def take_while(pattern)
66
+ start = @index
67
+ @index += 1 while @source[@index]&.match?(pattern)
68
+ @source[start...@index]
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # Maintains source position and skips comments, POD, and whitespace.
6
+ module SourceScanner
7
+ private
8
+
9
+ def scan_end_marker
10
+ return unless @column == 1 && remaining.match?(/\A__(?:END|DATA)__\s*(?:\n|\z)/)
11
+
12
+ marker = remaining.match(/\A__(?:END|DATA)__[^\n]*(?:\n|\z)/)[0]
13
+ advance(marker)
14
+ @data = remaining
15
+ advance(remaining)
16
+ [:DATA, @data]
17
+ end
18
+
19
+ def skip_ignored
20
+ loop do
21
+ skipped = skip_horizontal || skip_comment || skip_pod || skip_newline
22
+ break unless skipped
23
+ end
24
+ end
25
+
26
+ def skip_horizontal
27
+ text = remaining.match(/\A[^\S\n]+/)&.[](0)
28
+ advance(text) if text
29
+ end
30
+
31
+ def skip_comment
32
+ return unless current == '#'
33
+
34
+ advance(remaining.match(/\A[^\n]*/)[0])
35
+ end
36
+
37
+ def skip_pod
38
+ return unless @column == 1 && @state.expect == :state && remaining.match?(/\A=\w/)
39
+
40
+ finish = remaining.match(/^=cut\s*$\n?/)&.end(0)
41
+ raise CompileError, location_message('POD document has no =cut') unless finish
42
+
43
+ advance(remaining[0...finish])
44
+ end
45
+
46
+ def skip_newline
47
+ return unless current == "\n"
48
+
49
+ advance("\n")
50
+ consume_heredocs unless @pending_heredocs.empty?
51
+ true
52
+ end
53
+
54
+ def take_match(pattern)
55
+ match = pattern.match(remaining)&.[](0)
56
+ advance(match) if match
57
+ end
58
+
59
+ def rewind(index, column)
60
+ @index = index
61
+ @column = column
62
+ nil
63
+ end
64
+
65
+ def term_starts?(character)
66
+ character&.match?(/["'$@%*(&\d]/)
67
+ end
68
+
69
+ def delimiter?(character)
70
+ character && !character.match?(/[A-Za-z0-9_]/)
71
+ end
72
+
73
+ def illegal_character
74
+ raise CompileError, location_message("Unrecognized character #{current.inspect}")
75
+ end
76
+
77
+ def location_message(message)
78
+ "#{message} at #{@file} line #{@line}."
79
+ end
80
+
81
+ def advance(text)
82
+ return unless text
83
+
84
+ @index += text.length
85
+ lines = text.count("\n")
86
+ @line += lines
87
+ @column = lines.zero? ? @column + text.length : text.length - text.rindex("\n")
88
+ text
89
+ end
90
+
91
+ def remaining
92
+ @source[@index..]
93
+ end
94
+
95
+ def current
96
+ @source[@index]
97
+ end
98
+
99
+ def end?
100
+ @index >= @source.length
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # Tracks whether the lexer expects a term, operator, statement, block, or reference.
6
+ class State
7
+ STATES = %i[state term operator block ref].freeze
8
+ Frame = Data.define(:open, :kind, :line)
9
+
10
+ attr_reader :brackets
11
+ attr_accessor :expect
12
+
13
+ def initialize
14
+ @expect = :state
15
+ @brackets = []
16
+ end
17
+
18
+ def push(open, kind, line)
19
+ @brackets << Frame.new(open, kind, line)
20
+ end
21
+
22
+ def pop(close)
23
+ frame = @brackets.pop
24
+ raise CompileError, "Unmatched #{close}" unless frame
25
+
26
+ frame
27
+ end
28
+
29
+ def term!
30
+ @expect = :operator
31
+ end
32
+
33
+ def operator!
34
+ @expect = :term
35
+ end
36
+
37
+ def statement!
38
+ @expect = :state
39
+ end
40
+
41
+ def block!
42
+ @expect = :block
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # Scans operators, brackets, and heredoc declarations and bodies.
6
+ module StructureScanner
7
+ private
8
+
9
+ def scan_bracket
10
+ character = current
11
+ return open_bracket(character) if '({['.include?(character)
12
+ return close_bracket(character) if ')}]'.include?(character)
13
+
14
+ nil
15
+ end
16
+
17
+ def open_bracket(character)
18
+ @sort_comparator_pending = false if @sort_comparator_pending && character == '{'
19
+ kind, type = bracket_kind(character)
20
+ advance(character)
21
+ @state.push(character, kind, @line)
22
+ kind == :block ? @state.statement! : @state.operator!
23
+ [type, character]
24
+ end
25
+
26
+ def bracket_kind(character)
27
+ return anonymous_sub_block if @sub_name_pending && character == '{'
28
+
29
+ return arrow_bracket(character) if @after_arrow
30
+
31
+ return [@state.expect == :operator ? :subscript : :anon_array, :'['] if character == '['
32
+
33
+ if character == '('
34
+ kind = @condition_pending ? :condition : :paren
35
+ @condition_pending = false
36
+ return [kind, :'(']
37
+ end
38
+ return %i[deref_block {] if @indirect_print
39
+ return %i[subscript SUBSCRIPT_LBRACE] if @state.expect == :operator
40
+ return %i[block {] if @state.expect == :block
41
+
42
+ hash = intuit_hash?
43
+ [hash ? :anon_hash : :block, hash ? :HASHREF_LBRACE : :'{']
44
+ end
45
+
46
+ def anonymous_sub_block
47
+ @sub_name_pending = false
48
+ %i[anon_sub {]
49
+ end
50
+
51
+ def arrow_bracket(character)
52
+ @after_arrow = false
53
+ return [:subscript, :'['] if character == '['
54
+ return %i[subscript SUBSCRIPT_LBRACE] if character == '{'
55
+
56
+ [:paren, :'(']
57
+ end
58
+
59
+ def close_bracket(character)
60
+ frame = @state.pop(character)
61
+ advance(character)
62
+ if frame.kind == :block
63
+ @state.statement!
64
+ elsif frame.kind == :condition
65
+ @state.block!
66
+ else
67
+ @state.term!
68
+ end
69
+ [character.to_sym, character]
70
+ end
71
+
72
+ def intuit_hash?
73
+ body = remaining[1..]
74
+ body.match?(/\A\s*}/) || body.match?(/\A\s*(?:[\w$]+|'[^']*'|"[^"]*"|\d+)\s*(?:=>|,|})/)
75
+ end
76
+
77
+ def scan_operator
78
+ operator = SORTED_OPERATORS.find { |candidate| remaining.start_with?(candidate) }
79
+ return unless operator
80
+ return if operator == '/' && @state.expect != :operator
81
+
82
+ advance(operator)
83
+ type = OPERATORS.fetch(operator)
84
+ @after_arrow = true if type == :ARROW
85
+ @indirect_print = false if type == :';'
86
+ type == :';' ? @state.statement! : operator_term_transition(type)
87
+ [type, operator]
88
+ end
89
+
90
+ def operator_term_transition(type)
91
+ %i[INC DEC].include?(type) ? @state.term! : @state.operator!
92
+ end
93
+
94
+ def operator_type?(type)
95
+ %i[REPEAT SLT SGT SLE SGE SEQ SNE SCMP AND OR XOR].include?(type)
96
+ end
97
+
98
+ def scan_heredoc
99
+ return unless @state.expect != :operator && remaining.start_with?('<<')
100
+
101
+ match = remaining.match(/\A<<(~?)(?:(["'])([^"']+)\2|\\?([A-Za-z_]\w*))/)
102
+ return unless match
103
+
104
+ advance(match[0])
105
+ heredoc = Heredoc.new(terminator: match[3] || match[4], interpolate: heredoc_interpolates?(match),
106
+ indent: !match[1].empty?)
107
+ @pending_heredocs << heredoc
108
+ @state.term!
109
+ [:HEREDOC, heredoc]
110
+ end
111
+
112
+ def heredoc_interpolates?(match)
113
+ match[2] != "'" && !match[0].start_with?('<<\\')
114
+ end
115
+
116
+ def consume_heredocs
117
+ @pending_heredocs.each { |heredoc| consume_heredoc(heredoc) }
118
+ @pending_heredocs.clear
119
+ end
120
+
121
+ def consume_heredoc(heredoc)
122
+ lines = []
123
+ loop do
124
+ missing_heredoc!(heredoc) if end?
125
+ line = remaining.match(/\A.*(?:\n|\z)/)[0]
126
+ advance(line)
127
+ break if heredoc_end?(line.chomp, heredoc)
128
+
129
+ lines << line
130
+ end
131
+ heredoc.body = heredoc.indent ? dedent(lines) : lines.join
132
+ end
133
+
134
+ def missing_heredoc!(heredoc)
135
+ message = "Can't find string terminator #{heredoc.terminator.inspect} anywhere before EOF"
136
+ raise CompileError, location_message(message)
137
+ end
138
+
139
+ def heredoc_end?(line, heredoc)
140
+ heredoc.indent ? line.lstrip == heredoc.terminator : line == heredoc.terminator
141
+ end
142
+
143
+ def dedent(lines)
144
+ width = lines.reject { |line| line.strip.empty? }.map { |line| line[/\A\s*/].length }.min || 0
145
+ lines.map { |line| line.sub(/\A[ \t]{0,#{width}}/, '') }.join
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # Scans literals, variables, words, and quote-like terms.
6
+ module TermScanner # rubocop:disable Metrics/ModuleLength
7
+ private
8
+
9
+ def scan_number
10
+ scanned = Number.scan(remaining)
11
+ return unless scanned
12
+
13
+ literal, value = scanned
14
+ advance(literal)
15
+ @module_name_pending = false
16
+ @state.term!
17
+ [literal.start_with?('v') ? :VERSION : :NUMBER, value]
18
+ end
19
+
20
+ # rubocop:disable-next Metrics/AbcSize
21
+ def scan_variable
22
+ sigil = current
23
+ return unless SIGILS.key?(sigil)
24
+ return if @state.expect == :operator && sigil != '$'
25
+
26
+ start = @index
27
+ column = @column
28
+ advance(sigil)
29
+ return scan_braced_dereference(sigil, start) if braced_dereference?
30
+ return scan_braced_variable(sigil, start) if current == '{'
31
+ return finish_variable(sigil, start, advance('$') && '$') if sigil == '$' && current == '$' &&
32
+ !@source[@index + 1].to_s.match?(/[\w$]/)
33
+
34
+ advance('$') while current == '$'
35
+ advance('#') if sigil == '$' && current == '#'
36
+ name = scan_variable_name
37
+ return rewind(start, column) unless name
38
+
39
+ finish_variable(sigil, start, name)
40
+ end
41
+
42
+ def finish_variable(sigil, start, name)
43
+ @after_arrow = false
44
+ @state.term!
45
+ type = variable_type(sigil, @source[start...@index])
46
+ type = :FILEHANDLE if @indirect_print && term_starts?(remaining.lstrip[0])
47
+ @indirect_print = false
48
+ [type, name]
49
+ end
50
+
51
+ def scan_braced_variable(sigil, start)
52
+ advance('{')
53
+ name = take_match(/\A(?:\^[A-Z_]+|[A-Za-z_]\w*(?:::\w+)*)/)
54
+ raise CompileError, location_message('Bad name after sigil') unless name && current == '}'
55
+
56
+ advance('}')
57
+ @state.term!
58
+ [variable_type(sigil, @source[start...@index]), name]
59
+ end
60
+
61
+ def scan_braced_dereference(sigil, _start)
62
+ advance('{')
63
+ @state.push('{', :subscript, @line)
64
+ @state.operator!
65
+ [{ '$' => :SCALAR_DEREF_OPEN, '@' => :ARRAY_DEREF_OPEN,
66
+ '%' => :HASH_DEREF_OPEN, '&' => :CODE_DEREF_OPEN }.fetch(sigil), sigil]
67
+ end
68
+
69
+ def braced_dereference?
70
+ return false unless current == '{'
71
+
72
+ next_character = @source[(@index + 1)..].lstrip[0]
73
+ SIGILS.key?(@source[@index + 1]) || (next_character && '{[('.include?(next_character))
74
+ end
75
+
76
+ def scan_variable_name
77
+ take_match(/\A(?:\^[A-Z_]+|\d+|[A-Za-z_]\w*(?:::\w+)*|[^\w\s])/)
78
+ end
79
+
80
+ def variable_type(sigil, literal)
81
+ return :ARRAY_LAST if literal.start_with?('$#')
82
+ return :SPECIAL_VAR if literal == '$$' || literal.match?(%r{\A\$[\d_@!/\\,";.?|^`'&+-]})
83
+ return :SCALAR_DEREF if literal.start_with?('$$')
84
+ return dereference_type(sigil) if literal[1] == '$'
85
+
86
+ SIGILS.fetch(sigil)
87
+ end
88
+
89
+ def dereference_type(sigil)
90
+ { '$' => :SCALAR_DEREF, '@' => :ARRAY_DEREF, '%' => :HASH_DEREF, '&' => :CODE_DEREF }.fetch(sigil)
91
+ end
92
+
93
+ def scan_word # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
94
+ word = remaining.match(/\A[A-Za-z_]\w*(?:::\w+)*(?:::)?/)&.[](0)
95
+ return unless word
96
+ return scan_quote_operator(word) if quote_operator?(word) && !@sub_name_pending && !@after_arrow
97
+
98
+ advance(word)
99
+ sub_name = @sub_name_pending
100
+ package_name = @package_name_pending
101
+ type = if package_name
102
+ @package_name_pending = false
103
+ :PACKAGE_NAME
104
+ elsif @module_name_pending && word != 'constant'
105
+ @module_name_pending = false
106
+ :PACKAGE_NAME
107
+ else
108
+ @module_name_pending = false if word == 'constant'
109
+ sub_name ? :SUB_NAME : word_type(word)
110
+ end
111
+ @module_name_pending = true if %i[USE NO].include?(type)
112
+ @package_name_pending = true if type == :PACKAGE
113
+ expression = scan_map_expression(type)
114
+ return expression if expression
115
+
116
+ @sub_name_pending = false
117
+ @prototype_pending = sub_name && remaining.lstrip.start_with?('(')
118
+ if @sort_comparator_pending
119
+ type = :SORT_SUB if type == :FUNC
120
+ @sort_comparator_pending = false
121
+ end
122
+ @after_arrow = false
123
+ transition_word(type)
124
+ @state.block! if package_name && remaining.lstrip.start_with?('{')
125
+ [type, word]
126
+ end
127
+
128
+ def scan_map_expression(type)
129
+ return unless %i[MAP GREP].include?(type) && !remaining.lstrip.start_with?('{')
130
+
131
+ comma = top_level_comma(remaining)
132
+ return unless comma
133
+
134
+ source = remaining[0...comma].strip
135
+ advance(remaining[0..comma])
136
+ @indirect_print = false
137
+ @state.operator!
138
+ [type == :MAP ? :MAP_EXPR : :GREP_EXPR, source]
139
+ end
140
+
141
+ def top_level_comma(source)
142
+ brackets = []
143
+ quote = nil
144
+ escaped = false
145
+ source.each_char.with_index do |character, index|
146
+ if escaped
147
+ escaped = false
148
+ elsif character == '\\'
149
+ escaped = true
150
+ elsif quote
151
+ quote = nil if character == quote
152
+ elsif %w[' "].include?(character)
153
+ quote = character
154
+ elsif '([{'.include?(character)
155
+ brackets << character
156
+ elsif ')]}'.include?(character)
157
+ brackets.pop
158
+ elsif character == ',' && brackets.empty?
159
+ return index
160
+ end
161
+ end
162
+ nil
163
+ end
164
+
165
+ def scan_prototype
166
+ anonymous = @sub_name_pending && current == '('
167
+ return unless @prototype_pending || anonymous
168
+
169
+ prototype = remaining.match(/\A\(([^)]*)\)/)
170
+ raise CompileError, location_message('Malformed prototype') unless prototype
171
+
172
+ value = prototype[1].gsub(/\s+/, '')
173
+ raise CompileError, location_message('Illegal character in prototype') unless value.match?(/\A[$@%&*+_;\\]*\z/)
174
+
175
+ advance(prototype[0])
176
+ @prototype_pending = false
177
+ @sub_name_pending = false if anonymous
178
+ @state.block!
179
+ [:PROTOTYPE, value]
180
+ end
181
+
182
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
183
+ def word_type(word)
184
+ return word.upcase.to_sym if %w[print printf say].include?(word)
185
+ return :BAREWORD if @after_arrow
186
+
187
+ callable = Keywords::NAMED_UNARY.include?(word) || Keywords::LISTOP.include?(word)
188
+ return :FUNC if callable && remaining.lstrip.start_with?('(')
189
+
190
+ keyword = Keywords.type(word, @state.expect)
191
+ return keyword if keyword
192
+
193
+ if @known_subs.key?(word)
194
+ return :LISTOP if @known_subs[word] == :listop && !remaining.lstrip.start_with?('(')
195
+
196
+ return :FUNC
197
+ end
198
+ return :PACKAGE_NAME if remaining.lstrip.start_with?('->')
199
+ return :FILEHANDLE if @indirect_print && word.match?(/\A[A-Z][A-Z0-9_]*\z/)
200
+ return :STRING if remaining.lstrip.start_with?('=>')
201
+ return :FUNC if word.include?('::') && remaining.lstrip.start_with?('(')
202
+ return :PACKAGE_NAME if word.include?('::')
203
+ return :FUNC if remaining.lstrip.start_with?('(')
204
+
205
+ :BAREWORD
206
+ end
207
+
208
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
209
+ def transition_word(type)
210
+ @indirect_print = false if type == :FILEHANDLE
211
+ @indirect_print = false if type == :FUNC
212
+ @indirect_print = false if type == :LISTOP
213
+ if type == :SUB
214
+ @sub_name_pending = true
215
+ return @state.block!
216
+ end
217
+ return @state.block! if %i[SUB_NAME ELSE DO BEGIN CHECK INIT END CONTINUE].include?(type)
218
+ return remaining.lstrip.start_with?('{') ? @state.block! : @state.operator! if type == :EVAL
219
+
220
+ if %i[MAP GREP SORT].include?(type)
221
+ @indirect_print = false
222
+ block = remaining.lstrip.start_with?('{')
223
+ @sort_comparator_pending = true if type == :SORT && !block
224
+ return block ? @state.block! : @state.operator!
225
+ end
226
+
227
+ @condition_pending = true if %i[IF ELSIF UNLESS WHILE UNTIL FOR FOREACH].include?(type)
228
+ operators = %i[IF UNLESS WHILE UNTIL FOR FOREACH RETURN MY OUR LOCAL STATE PRINT PRINTF SAY LISTOP
229
+ SORT_SUB NAMED_UNARY NOT GOTO]
230
+ if operators.include?(type) || operator_type?(type)
231
+ @indirect_print = true if %i[PRINT PRINTF SAY].include?(type)
232
+ @state.operator!
233
+ else
234
+ @state.term!
235
+ end
236
+ end
237
+
238
+ def scan_quote
239
+ return scan_simple_quote if %w[' " `].include?(current)
240
+ return scan_regex if current == '/' && @state.expect != :operator
241
+
242
+ nil
243
+ end
244
+
245
+ def scan_simple_quote
246
+ delimiter = current
247
+ quote, finish = QuoteLike.new(@source, @index).scan(delimiter == '`' ? :backtick : :q)
248
+ advance(@source[@index...finish])
249
+ quote.interpolate = delimiter != "'"
250
+ @state.term!
251
+ [simple_quote_type(delimiter, quote), quote]
252
+ end
253
+
254
+ def simple_quote_type(delimiter, quote)
255
+ return :BACKTICK if delimiter == '`'
256
+
257
+ quote.interpolate ? :ISTRING : :STRING
258
+ end
259
+
260
+ def scan_regex
261
+ quote, finish = QuoteLike.new(@source, @index).scan(:m)
262
+ advance(@source[@index...finish])
263
+ @state.term!
264
+ [:MATCH, quote]
265
+ end
266
+
267
+ def quote_operator?(word)
268
+ QUOTE_OPERATORS.include?(word) && delimiter?(@source[@index + word.length])
269
+ end
270
+
271
+ def scan_quote_operator(word)
272
+ advance(word)
273
+ operator = word.to_sym
274
+ quote, finish = QuoteLike.new(@source, @index).scan(operator)
275
+ advance(@source[@index...finish])
276
+ @state.term!
277
+ types = { q: :STRING, qq: :ISTRING, qw: :QW, qr: :QR, m: :MATCH, s: :SUBST, tr: :TRANS, y: :TRANS }
278
+ [types.fetch(operator), quote]
279
+ end
280
+
281
+ def scan_filetest
282
+ return unless @state.expect != :operator && (literal = FILETEST.match(remaining)&.[](0))
283
+
284
+ advance(literal)
285
+ @state.operator!
286
+ [:FILETEST, literal]
287
+ end
288
+
289
+ def scan_readline
290
+ return if @state.expect == :operator
291
+
292
+ literal = remaining.match(/\A<(?:\$?[A-Za-z_]\w*(?:::\w+)*)?>/)&.[](0)
293
+ return unless literal
294
+
295
+ advance(literal)
296
+ @state.term!
297
+ [:READLINE, literal[1...-1]]
298
+ end
299
+ end
300
+ end
301
+ end