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,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