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.
- checksums.yaml +7 -0
- data/.rubocop.yml +35 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/Rakefile +92 -0
- data/bin/peruby +9 -0
- data/doc/COMPAT.md +54 -0
- data/doc/CONTRIBUTING.md +21 -0
- data/doc/DESIGN.md +25 -0
- data/doc/INCOMPATIBILITIES.md +30 -0
- data/doc/PERF.md +51 -0
- data/doc/ROADMAP.md +18 -0
- data/examples/hello.pl +1 -0
- data/examples/json.pl +2 -0
- data/examples/object.pl +5 -0
- data/examples/word_count.pl +6 -0
- data/lib/peruby/cli.rb +224 -0
- data/lib/peruby/compile_unit.rb +103 -0
- data/lib/peruby/compiler.rb +224 -0
- data/lib/peruby/errors.rb +40 -0
- data/lib/peruby/lexer/heredoc.rb +8 -0
- data/lib/peruby/lexer/keywords.rb +63 -0
- data/lib/peruby/lexer/number.rb +32 -0
- data/lib/peruby/lexer/quote_like.rb +72 -0
- data/lib/peruby/lexer/source_scanner.rb +104 -0
- data/lib/peruby/lexer/state.rb +46 -0
- data/lib/peruby/lexer/structure_scanner.rb +149 -0
- data/lib/peruby/lexer/term_scanner.rb +301 -0
- data/lib/peruby/lexer/token.rb +16 -0
- data/lib/peruby/lexer.rb +123 -0
- data/lib/peruby/node.rb +88 -0
- data/lib/peruby/op/assign.rb +128 -0
- data/lib/peruby/op/builtin.rb +903 -0
- data/lib/peruby/op/call.rb +378 -0
- data/lib/peruby/op/control.rb +256 -0
- data/lib/peruby/op/element.rb +136 -0
- data/lib/peruby/op/expression.rb +342 -0
- data/lib/peruby/op/io.rb +113 -0
- data/lib/peruby/op/list.rb +102 -0
- data/lib/peruby/op/literal.rb +84 -0
- data/lib/peruby/op/loop.rb +158 -0
- data/lib/peruby/op/regexp.rb +288 -0
- data/lib/peruby/op/variable.rb +534 -0
- data/lib/peruby/op.rb +47 -0
- data/lib/peruby/parser/grammar.rb +5797 -0
- data/lib/peruby/parser/grammar.y +576 -0
- data/lib/peruby/parser.rb +14 -0
- data/lib/peruby/runtime/code.rb +21 -0
- data/lib/peruby/runtime/conv.rb +140 -0
- data/lib/peruby/runtime/directory_handle.rb +18 -0
- data/lib/peruby/runtime/env.rb +129 -0
- data/lib/peruby/runtime/glob.rb +24 -0
- data/lib/peruby/runtime/interpolation.rb +223 -0
- data/lib/peruby/runtime/io_handle.rb +37 -0
- data/lib/peruby/runtime/local_stack.rb +90 -0
- data/lib/peruby/runtime/match_state.rb +62 -0
- data/lib/peruby/runtime/module_loader.rb +133 -0
- data/lib/peruby/runtime/mro.rb +94 -0
- data/lib/peruby/runtime/perl_array.rb +81 -0
- data/lib/peruby/runtime/perl_hash.rb +57 -0
- data/lib/peruby/runtime/ref.rb +43 -0
- data/lib/peruby/runtime/regexp_compiler.rb +75 -0
- data/lib/peruby/runtime/scalar.rb +27 -0
- data/lib/peruby/runtime/sprintf.rb +54 -0
- data/lib/peruby/runtime/stash.rb +50 -0
- data/lib/peruby/runtime/test_builder.rb +47 -0
- data/lib/peruby/runtime.rb +325 -0
- data/lib/peruby/validator.rb +236 -0
- data/lib/peruby/version.rb +5 -0
- data/lib/peruby.rb +31 -0
- data/t/00-basic.t +5 -0
- data/t/lib/MiniTest.pm +22 -0
- 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
|