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,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
module Op
|
|
5
|
+
# Constant scalar or quote-like value.
|
|
6
|
+
class Literal < Base
|
|
7
|
+
def initialize(value)
|
|
8
|
+
@value = value
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(env, context)
|
|
12
|
+
value = if @value.is_a?(Lexer::Quote) && @value.operator == :backtick
|
|
13
|
+
command = Interpolation.expand(@value, env)
|
|
14
|
+
output = IO.popen(command, &:read)
|
|
15
|
+
env.fetch(:scalar, '?').set($?.to_i) # rubocop:disable Style/SpecialGlobalVars
|
|
16
|
+
output
|
|
17
|
+
elsif @value.is_a?(Lexer::Heredoc)
|
|
18
|
+
quote = Lexer::Quote.new(operator: :heredoc, parts: [@value.body], modifiers: '',
|
|
19
|
+
interpolate: @value.interpolate)
|
|
20
|
+
Interpolation.expand(quote, env)
|
|
21
|
+
elsif @value.is_a?(Lexer::Quote)
|
|
22
|
+
Interpolation.expand(@value, env)
|
|
23
|
+
else
|
|
24
|
+
@value
|
|
25
|
+
end
|
|
26
|
+
context == :list ? [Scalar.new(value)] : value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_callable
|
|
30
|
+
return super if @value.is_a?(Lexer::Quote) && @value.operator == :backtick
|
|
31
|
+
return super if @value.is_a?(Lexer::Heredoc)
|
|
32
|
+
return interpolated_callable if @value.is_a?(Lexer::Quote) && simple_interpolation?
|
|
33
|
+
return super if @value.is_a?(Lexer::Quote)
|
|
34
|
+
|
|
35
|
+
value = @value
|
|
36
|
+
->(_env, context) { context == :list ? [Scalar.new(value)] : value }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def scalar_callable
|
|
40
|
+
return ->(_env) { @value } unless @value.is_a?(Lexer::Quote) || @value.is_a?(Lexer::Heredoc)
|
|
41
|
+
return if @value.is_a?(Lexer::Heredoc) || @value.operator == :backtick
|
|
42
|
+
return unless @value.is_a?(Lexer::Quote) && simple_interpolation?
|
|
43
|
+
|
|
44
|
+
callable = interpolated_callable
|
|
45
|
+
->(env) { callable.call(env, :scalar) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def simple_interpolation?
|
|
51
|
+
source = @value.parts.first
|
|
52
|
+
complex = source.include?('$$') || source.include?('$#') || source.match?(/[$@](?:\{|\w+(?:->)?[\[{])/)
|
|
53
|
+
@value.interpolate && !source.include?('\\') && !complex
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def interpolated_callable
|
|
57
|
+
parts = interpolation_parts(@value.parts.first)
|
|
58
|
+
lambda do |env, context|
|
|
59
|
+
value = parts.each_with_object(+'') do |part, output|
|
|
60
|
+
if part.is_a?(String)
|
|
61
|
+
output << part
|
|
62
|
+
else
|
|
63
|
+
sigil, name = part
|
|
64
|
+
output << Interpolation.interpolate(env.fetch(sigil, name), sigil, env)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
context == :list ? [Scalar.new(value)] : value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def interpolation_parts(source)
|
|
72
|
+
parts = []
|
|
73
|
+
offset = 0
|
|
74
|
+
while (match = Interpolation.variable_pattern.match(source, offset))
|
|
75
|
+
parts << source[offset...match.begin(0)] unless match.begin(0) == offset
|
|
76
|
+
parts << [match[1] == '$' ? :scalar : :array, match[2] || match[3]]
|
|
77
|
+
offset = match.end(0)
|
|
78
|
+
end
|
|
79
|
+
parts << source[offset..] if offset < source.length
|
|
80
|
+
parts
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
module Op
|
|
5
|
+
# Non-local loop control, optionally targeting a label.
|
|
6
|
+
class LoopJump < Base
|
|
7
|
+
def initialize(kind, label)
|
|
8
|
+
@kind = kind
|
|
9
|
+
@label = label
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run(_env, _context) = raise LoopControl.new(@kind, @label)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# C-style for loop.
|
|
16
|
+
class CFor < Base
|
|
17
|
+
include LoopExecution
|
|
18
|
+
|
|
19
|
+
def initialize(initializer, condition, step, body, continuation, label) # rubocop:disable Metrics/ParameterLists
|
|
20
|
+
@initializer = initializer
|
|
21
|
+
@condition = condition
|
|
22
|
+
@step = step
|
|
23
|
+
@body = body
|
|
24
|
+
@continue = continuation
|
|
25
|
+
@label = label
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(env, _context)
|
|
29
|
+
env.with_scope do
|
|
30
|
+
@initializer&.run(env, :void)
|
|
31
|
+
while @condition.nil? || Conv.truthy?(@condition.run(env, :scalar))
|
|
32
|
+
break unless run_iteration?(env)
|
|
33
|
+
|
|
34
|
+
@step&.run(env, :void)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Stable Perl sort with optional block or named comparator.
|
|
42
|
+
class Sort < Base
|
|
43
|
+
def initialize(comparator, list, package)
|
|
44
|
+
@comparator = comparator
|
|
45
|
+
@list = list
|
|
46
|
+
@package = package
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run(env, context)
|
|
50
|
+
values = @list.run(env, :list)
|
|
51
|
+
sorted = values.each_with_index.sort do |(left, left_index), (right, right_index)|
|
|
52
|
+
comparison = compare(left, right, env)
|
|
53
|
+
comparison.zero? ? left_index <=> right_index : comparison
|
|
54
|
+
end.map(&:first)
|
|
55
|
+
context == :list ? sorted : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
61
|
+
def compare(left, right, env)
|
|
62
|
+
return Conv.to_str(left.get) <=> Conv.to_str(right.get) unless @comparator
|
|
63
|
+
|
|
64
|
+
a = env.runtime.stash.glob("#{@package}::a").scalar
|
|
65
|
+
b = env.runtime.stash.glob("#{@package}::b").scalar
|
|
66
|
+
previous = [a.get, b.get]
|
|
67
|
+
a.set(left.get)
|
|
68
|
+
b.set(right.get)
|
|
69
|
+
result = if @comparator.is_a?(String)
|
|
70
|
+
code = env.runtime.stash.glob(@comparator).code
|
|
71
|
+
raise PerlError, "Undefined sort subroutine #{@comparator}" unless code
|
|
72
|
+
|
|
73
|
+
env.runtime.call(code, [], :scalar)
|
|
74
|
+
else
|
|
75
|
+
@comparator.run(env, :scalar)
|
|
76
|
+
end
|
|
77
|
+
Conv.to_num(result).to_i
|
|
78
|
+
ensure
|
|
79
|
+
a&.set(previous&.first)
|
|
80
|
+
b&.set(previous&.last)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Foreach loop with a fresh aliased lexical scalar each iteration.
|
|
85
|
+
class ForEach < Base
|
|
86
|
+
include LoopExecution
|
|
87
|
+
|
|
88
|
+
def initialize(sigil, name, list, body, continuation = nil, label = nil) # rubocop:disable Metrics/ParameterLists
|
|
89
|
+
@sigil = sigil
|
|
90
|
+
@name = name
|
|
91
|
+
@list = list
|
|
92
|
+
@body = body
|
|
93
|
+
@continue = continuation
|
|
94
|
+
@label = label
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def run(env, _context)
|
|
98
|
+
@list.run(env, :list).each do |cell|
|
|
99
|
+
keep_going = true
|
|
100
|
+
env.with_scope do
|
|
101
|
+
env.bind(@sigil, @name, cell)
|
|
102
|
+
keep_going = run_iteration?(env)
|
|
103
|
+
end
|
|
104
|
+
break unless keep_going
|
|
105
|
+
end
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Statement modifier: EXPR for LIST, with $_ aliased to each source cell.
|
|
111
|
+
class ModifierFor < Base
|
|
112
|
+
include LoopExecution
|
|
113
|
+
|
|
114
|
+
def initialize(expression, list)
|
|
115
|
+
@expression = expression
|
|
116
|
+
@body = expression
|
|
117
|
+
@list = list
|
|
118
|
+
@label = nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def run(env, _context)
|
|
122
|
+
@list.run(env, :list).each do |cell|
|
|
123
|
+
keep_going = true
|
|
124
|
+
env.with_scope do
|
|
125
|
+
env.bind(:scalar, '_', cell)
|
|
126
|
+
keep_going = run_iteration?(env)
|
|
127
|
+
end
|
|
128
|
+
break unless keep_going
|
|
129
|
+
end
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# map/grep block with $_ aliasing.
|
|
135
|
+
class Map < Base
|
|
136
|
+
def initialize(body, list, grep)
|
|
137
|
+
@body = body
|
|
138
|
+
@list = list
|
|
139
|
+
@grep = grep
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def run(env, context)
|
|
143
|
+
values = @list.run(env, :list).flat_map do |cell|
|
|
144
|
+
result = env.with_scope do
|
|
145
|
+
env.bind(:scalar, '_', cell)
|
|
146
|
+
@body.run(env, @grep ? :scalar : :list)
|
|
147
|
+
end
|
|
148
|
+
if @grep
|
|
149
|
+
Conv.truthy?(result) ? [cell] : []
|
|
150
|
+
else
|
|
151
|
+
Array(result)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
context == :list ? values : values.length
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
module Op
|
|
5
|
+
# Regexp match, using $_ when no explicit binding subject exists.
|
|
6
|
+
class Match < Base
|
|
7
|
+
def initialize(quote)
|
|
8
|
+
@quote = quote
|
|
9
|
+
return unless quote
|
|
10
|
+
|
|
11
|
+
source = quote.parts.first
|
|
12
|
+
@regexp = RegexpCompiler.compile(source, quote.modifiers) unless source.match?(Interpolation.variable_pattern)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(env, context)
|
|
16
|
+
match_on(env.fetch(:scalar, '_'), env, context)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def match_on(cell, env, context)
|
|
20
|
+
regexp = @regexp || RegexpCompiler.compile(Interpolation.pattern(@quote, env), @quote.modifiers)
|
|
21
|
+
match_regexp(regexp, cell, env, context, global: @quote.modifiers.include?('g'))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def match_regexp(regexp, cell, env, context, global: false)
|
|
25
|
+
return global(regexp, cell, env, context) if global
|
|
26
|
+
|
|
27
|
+
match = regexp.match(Conv.to_str(cell.get))
|
|
28
|
+
env.runtime.match!(match, Conv.to_str(cell.get)) if match
|
|
29
|
+
result(match, context)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def match_callable
|
|
33
|
+
return if !@regexp || @quote.modifiers.include?('g')
|
|
34
|
+
|
|
35
|
+
regexp = @regexp
|
|
36
|
+
lambda do |value, env, context|
|
|
37
|
+
subject = value
|
|
38
|
+
subject = Conv.to_str(subject) unless subject.is_a?(String)
|
|
39
|
+
match = regexp.match(subject)
|
|
40
|
+
env.runtime.match!(match, subject) if match
|
|
41
|
+
next match ? 1 : '' unless context == :list
|
|
42
|
+
|
|
43
|
+
values = if match
|
|
44
|
+
match.captures.empty? ? [1] : match.captures
|
|
45
|
+
else
|
|
46
|
+
[]
|
|
47
|
+
end
|
|
48
|
+
values.map { |value| Scalar.new(value) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def global(regexp, cell, env, context)
|
|
55
|
+
return global_list(regexp, cell, env) if context == :list
|
|
56
|
+
|
|
57
|
+
subject = Conv.to_str(cell.get)
|
|
58
|
+
match = regexp.match(subject, cell.pos || 0)
|
|
59
|
+
unless match
|
|
60
|
+
cell.pos = nil
|
|
61
|
+
return ''
|
|
62
|
+
end
|
|
63
|
+
env.runtime.match!(match, subject)
|
|
64
|
+
cell.pos = next_position(match, subject)
|
|
65
|
+
1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def global_list(regexp, cell, env)
|
|
69
|
+
subject = Conv.to_str(cell.get)
|
|
70
|
+
matches = []
|
|
71
|
+
position = 0
|
|
72
|
+
while (match = regexp.match(subject, position))
|
|
73
|
+
env.runtime.match!(match, subject)
|
|
74
|
+
values = match.captures.empty? ? [match[0]] : match.captures
|
|
75
|
+
matches.concat(values.map { |value| Scalar.new(value) })
|
|
76
|
+
position = next_position(match, subject)
|
|
77
|
+
end
|
|
78
|
+
matches
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def next_position(match, subject)
|
|
82
|
+
return match.end(0) unless match.begin(0) == match.end(0)
|
|
83
|
+
|
|
84
|
+
[match.end(0) + 1, subject.length + 1].min
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def result(match, context)
|
|
88
|
+
return match ? 1 : '' unless context == :list
|
|
89
|
+
return [] unless match
|
|
90
|
+
|
|
91
|
+
values = match.captures.empty? ? [1] : match.captures
|
|
92
|
+
values.map { |value| Scalar.new(value) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Explicit =~ or !~ binding.
|
|
97
|
+
class Bind < Base
|
|
98
|
+
def initialize(subject, pattern, negated)
|
|
99
|
+
@subject = subject
|
|
100
|
+
@pattern = pattern
|
|
101
|
+
@negated = negated
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def run(env, context)
|
|
105
|
+
bind(env, context) { @subject.run(env, :scalar) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def to_callable
|
|
109
|
+
subject = @subject.to_callable
|
|
110
|
+
scalar_subject = @subject.scalar_callable if @subject.respond_to?(:scalar_callable)
|
|
111
|
+
lvalue = @subject.lvalue_callable if @subject.respond_to?(:lvalue_callable)
|
|
112
|
+
match = @pattern.match_callable if !@negated && @pattern.respond_to?(:match_callable)
|
|
113
|
+
lambda do |env, context|
|
|
114
|
+
value = scalar_subject ? scalar_subject.call(env) : subject.call(env, :scalar)
|
|
115
|
+
cell = lvalue&.call(env)
|
|
116
|
+
next match.call(cell ? cell.value : value, env, context) if match
|
|
117
|
+
|
|
118
|
+
apply_bound(cell || Scalar.new(value), env, context)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def apply_bound(cell, env, context)
|
|
125
|
+
result = apply(cell, env, @negated ? :scalar : context)
|
|
126
|
+
return result unless @negated
|
|
127
|
+
|
|
128
|
+
value = Conv.truthy?(result) ? '' : 1
|
|
129
|
+
context == :list ? [Scalar.new(value)] : value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def bind(env, context)
|
|
133
|
+
cell = begin
|
|
134
|
+
@subject.lvalue(env, :scalar)
|
|
135
|
+
rescue PerlError
|
|
136
|
+
raise if @pattern.respond_to?(:lvalue_required?) && @pattern.lvalue_required?
|
|
137
|
+
|
|
138
|
+
Scalar.new(yield)
|
|
139
|
+
end
|
|
140
|
+
result = apply(cell, env, @negated ? :scalar : context)
|
|
141
|
+
return result unless @negated
|
|
142
|
+
|
|
143
|
+
value = Conv.truthy?(result) ? '' : 1
|
|
144
|
+
context == :list ? [Scalar.new(value)] : value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def apply(cell, env, context)
|
|
148
|
+
return @pattern.apply_to(cell, env, context) if @pattern.respond_to?(:apply_to)
|
|
149
|
+
return @pattern.match_on(cell, env, context) if @pattern.respond_to?(:match_on)
|
|
150
|
+
|
|
151
|
+
value = @pattern.run(env, :scalar)
|
|
152
|
+
regexp = value.is_a?(Ref) ? value.target : value
|
|
153
|
+
raise PerlError, 'Not a Regexp reference' unless regexp.is_a?(::Regexp)
|
|
154
|
+
|
|
155
|
+
Match.new(nil).match_regexp(regexp, cell, env, context)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Compiled qr// value.
|
|
160
|
+
class RegexpLiteral < Base
|
|
161
|
+
def initialize(quote)
|
|
162
|
+
@quote = quote
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def run(env, context)
|
|
166
|
+
value = Ref.new(RegexpCompiler.compile(Interpolation.pattern(@quote, env), @quote.modifiers))
|
|
167
|
+
context == :list ? [Scalar.new(value)] : value
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def match_on(cell, env, context)
|
|
171
|
+
Match.new(@quote).match_on(cell, env, context)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Perl s/// substitution, destructive unless /r is present.
|
|
176
|
+
class Substitute < Base
|
|
177
|
+
def initialize(quote)
|
|
178
|
+
@quote = quote
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def run(env, context)
|
|
182
|
+
apply_to(env.fetch(:scalar, '_'), env, context)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def lvalue_required? = !@quote.modifiers.include?('r')
|
|
186
|
+
|
|
187
|
+
def apply_to(cell, env, context)
|
|
188
|
+
subject = Conv.to_str(cell.get)
|
|
189
|
+
regexp = RegexpCompiler.compile(Interpolation.pattern(@quote, env), @quote.modifiers)
|
|
190
|
+
count = 0
|
|
191
|
+
replace = @quote.modifiers.include?('g') ? :gsub : :sub
|
|
192
|
+
result = subject.public_send(replace, regexp) do
|
|
193
|
+
count += 1
|
|
194
|
+
env.runtime.match!(Regexp.last_match, subject)
|
|
195
|
+
replacement(env)
|
|
196
|
+
end
|
|
197
|
+
value = if @quote.modifiers.include?('r')
|
|
198
|
+
result
|
|
199
|
+
else
|
|
200
|
+
cell.set(result)
|
|
201
|
+
count.zero? ? '' : count
|
|
202
|
+
end
|
|
203
|
+
context == :list ? [Scalar.new(value)] : value
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def replacement(env)
|
|
209
|
+
source = @quote.parts[1]
|
|
210
|
+
return evaluate(source, env) if @quote.modifiers.include?('e')
|
|
211
|
+
|
|
212
|
+
quote = Lexer::Quote.new(operator: :qq, parts: [source], modifiers: '', interpolate: true)
|
|
213
|
+
Interpolation.expand(quote, env)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def evaluate(source, env)
|
|
217
|
+
value = Compiler.new.compile(Parser.parse("#{source};", package: env.package)).run(env, :scalar)
|
|
218
|
+
return Conv.to_str(value) unless @quote.modifiers.scan('e').length > 1
|
|
219
|
+
|
|
220
|
+
tree = Parser.parse("#{Conv.to_str(value)};", package: env.package)
|
|
221
|
+
Conv.to_str(Compiler.new.compile(tree).run(env, :scalar))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Perl tr/// and y/// character translation.
|
|
226
|
+
class Transliterate < Base
|
|
227
|
+
def initialize(quote)
|
|
228
|
+
@quote = quote
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def run(env, context)
|
|
232
|
+
apply_to(env.fetch(:scalar, '_'), env, context)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def lvalue_required? = !@quote.modifiers.include?('r')
|
|
236
|
+
|
|
237
|
+
def apply_to(cell, _env, context)
|
|
238
|
+
source, replacement = @quote.parts
|
|
239
|
+
subject = Conv.to_str(cell.get)
|
|
240
|
+
search = @quote.modifiers.include?('c') ? "^#{source}" : source
|
|
241
|
+
count = subject.count(search)
|
|
242
|
+
result = translate(subject, search, replacement)
|
|
243
|
+
value = @quote.modifiers.include?('r') ? result : (cell.set(result) && count)
|
|
244
|
+
context == :list ? [Scalar.new(value)] : value
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
def translate(subject, search, replacement)
|
|
250
|
+
return subject if replacement.empty? && !@quote.modifiers.include?('d')
|
|
251
|
+
return subject.delete(search) if replacement.empty?
|
|
252
|
+
|
|
253
|
+
@quote.modifiers.include?('s') ? subject.tr_s(search, replacement) : subject.tr(search, replacement)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Perl split, including its special single-space pattern.
|
|
258
|
+
class Split < Base
|
|
259
|
+
def initialize(arguments)
|
|
260
|
+
@arguments = arguments
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def run(env, context)
|
|
264
|
+
pattern = @arguments[0]&.run(env, :scalar) || ' '
|
|
265
|
+
subject = @arguments[1]&.run(env, :scalar) || env.fetch(:scalar, '_').get
|
|
266
|
+
limit = @arguments[2]&.run(env, :scalar)
|
|
267
|
+
values = split(Conv.to_str(subject), pattern, limit)
|
|
268
|
+
return values.length unless context == :list
|
|
269
|
+
|
|
270
|
+
values.map { |value| Scalar.new(value) }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
|
|
275
|
+
def split(subject, pattern, limit)
|
|
276
|
+
pattern = pattern.target if pattern.is_a?(Ref)
|
|
277
|
+
return subject.strip.split(/\s+/, split_limit(limit)) if pattern == ' '
|
|
278
|
+
return subject.split('', split_limit(limit)) if pattern == ''
|
|
279
|
+
|
|
280
|
+
subject.split(pattern, split_limit(limit))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def split_limit(limit)
|
|
284
|
+
limit.nil? ? 0 : Conv.to_num(limit).to_i
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|