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,903 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'stringio'
5
+
6
+ module Peruby
7
+ module Op
8
+ # Standard-library-backed Perl built-ins that do not need custom syntax.
9
+ class Builtin < Base # rubocop:disable Metrics/ClassLength
10
+ NAMES = %w[
11
+ open close binmode seek tell fileno eof select opendir readdir closedir rewinddir stat lstat chmod chown utime
12
+ flock
13
+ glob
14
+ length substr index rindex uc lc ucfirst lcfirst reverse join sprintf
15
+ chomp chop chr ord quotemeta pop shift unshift splice keys values each sort defined scalar wantarray
16
+ abs int sqrt sin
17
+ cos atan2 exp log hex oct rand srand time localtime gmtime sleep times pack unpack mkdir rmdir unlink rename
18
+ link
19
+ symlink readlink truncate system exec fork wait waitpid kill exit getppid warn die first sum sum0 min max
20
+ minstr maxstr reduce any all none shuffle uniq pairs encode decode gettimeofday tempfile tempdir GetOptions
21
+ INT_MAX O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC SEEK_SET SEEK_CUR SEEK_END EACCES ENOENT
22
+ plan ok is isnt like is_deeply pass fail subtest done_testing diag
23
+ blessed reftype refaddr weaken dualvar looks_like_number floor ceil fmod strftime Dumper
24
+ basename dirname catfile catdir rel2abs getcwd abs_path mkpath rmtree encode_json decode_json
25
+ md5_hex sha1_hex sha256_hex dclone carp croak confess cluck
26
+ ].freeze
27
+ MODULE_NAMES = %w[
28
+ first sum sum0 min max minstr maxstr reduce any all none shuffle uniq pairs encode decode gettimeofday
29
+ tempfile tempdir GetOptions INT_MAX O_RDONLY O_WRONLY O_RDWR O_CREAT O_TRUNC SEEK_SET SEEK_CUR SEEK_END
30
+ EACCES ENOENT
31
+ ].freeze
32
+
33
+ def self.supports?(name) = NAMES.include?(name)
34
+
35
+ def initialize(name, arguments, file: '-e', line: 1)
36
+ @name = name
37
+ @arguments = arguments
38
+ @file = file
39
+ @line = line
40
+ end
41
+
42
+ def run(env, context)
43
+ code = env.runtime.stash.fetch(@name)&.code if MODULE_NAMES.include?(@name)
44
+ return env.runtime.call(code, @arguments.flat_map { |argument| argument.argument_cells(env) }, context) if code
45
+
46
+ result = dispatch(env, context)
47
+ return result if result.is_a?(Array) && result.all?(Scalar)
48
+
49
+ context == :list ? Array(result).map { |value| Scalar.new(value) } : result
50
+ end
51
+
52
+ def lvalue(env, context)
53
+ return super unless @name == 'substr'
54
+
55
+ cell = @arguments.first.lvalue(env, :scalar)
56
+ offset = Conv.to_num(@arguments[1].run(env, :scalar)).to_i
57
+ length = @arguments[2] && Conv.to_num(@arguments[2].run(env, :scalar)).to_i
58
+ SubstringCell.new(cell, offset, length)
59
+ end
60
+
61
+ private
62
+
63
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
64
+ def dispatch(env, context)
65
+ args = scalar_values(env)
66
+ case @name
67
+ when 'length' then Conv.to_str(args[0]).length
68
+ when 'uc' then Conv.to_str(args[0]).upcase
69
+ when 'lc' then Conv.to_str(args[0]).downcase
70
+ when 'ucfirst' then Conv.to_str(args[0]).sub(/./, &:upcase)
71
+ when 'lcfirst' then Conv.to_str(args[0]).sub(/./, &:downcase)
72
+ when 'chr' then Conv.to_num(args[0]).to_i.chr(Encoding::UTF_8)
73
+ when 'ord' then Conv.to_str(args[0]).codepoints.first || 0
74
+ when 'quotemeta' then Regexp.escape(Conv.to_str(args[0]))
75
+ when 'index', 'rindex' then string_index(args)
76
+ when 'substr' then substring(env, args)
77
+ when 'join' then list_values(env).drop(1).map { |value| Conv.to_str(value) }.join(Conv.to_str(args[0]))
78
+ when 'sprintf' then Sprintf.format(Conv.to_str(args[0]), list_values(env).drop(1))
79
+ when 'reverse' then reverse(env, context)
80
+ when 'defined' then args[0].nil? ? '' : 1
81
+ when 'scalar' then @arguments.first&.run(env, :scalar)
82
+ when 'wantarray' then { list: 1, scalar: '', void: nil }.fetch(env.want)
83
+ when 'abs' then overloaded_abs(env, args[0])
84
+ when 'int' then Conv.to_num(args[0]).to_i
85
+ when 'sqrt' then Math.sqrt(Conv.to_num(args[0]))
86
+ when 'sin', 'cos', 'exp', 'log' then Math.public_send(@name, Conv.to_num(args[0]))
87
+ when 'atan2' then Math.atan2(Conv.to_num(args[0]), Conv.to_num(args[1]))
88
+ when 'first', 'reduce', 'any', 'all', 'none' then list_utility_block(env, context)
89
+ when 'sum', 'sum0' then list_sum(env)
90
+ when 'min' then list_values(env).min_by { |value| Conv.to_num(value) }
91
+ when 'max' then list_values(env).max_by { |value| Conv.to_num(value) }
92
+ when 'minstr' then list_values(env).min_by { |value| Conv.to_str(value) }
93
+ when 'maxstr' then list_values(env).max_by { |value| Conv.to_str(value) }
94
+ when 'shuffle' then list_values(env).shuffle
95
+ when 'uniq'
96
+ seen = {}
97
+ list_values(env).select { |value| !seen.key?(Conv.to_str(value)) && (seen[Conv.to_str(value)] = true) }
98
+ when 'pairs' then list_values(env).each_slice(2).map { |pair| pair_reference(pair) }
99
+ when 'plan' then env.runtime.test_builder.plan(Conv.to_num(args.last).to_i)
100
+ when 'ok' then env.runtime.test_builder.ok(Conv.truthy?(args[0]), Conv.to_str(args[1]))
101
+ when 'pass' then env.runtime.test_builder.ok(true, Conv.to_str(args[0]))
102
+ when 'fail' then env.runtime.test_builder.ok(false, Conv.to_str(args[0]))
103
+ when 'is' then env.runtime.test_builder.ok(equal?(args[0], args[1]), Conv.to_str(args[2]))
104
+ when 'isnt' then env.runtime.test_builder.ok(!equal?(args[0], args[1]), Conv.to_str(args[2]))
105
+ when 'like' then env.runtime.test_builder.ok(like?(args[0], args[1]), Conv.to_str(args[2]))
106
+ when 'is_deeply'
107
+ env.runtime.test_builder.ok(deep_value(args[0]) == deep_value(args[1]), Conv.to_str(args[2]))
108
+ when 'done_testing' then env.runtime.test_builder.done_testing
109
+ when 'subtest' then subtest(env, args)
110
+ when 'diag' then env.runtime.stderr.puts("# #{Conv.to_str(args[0])}") && 1
111
+ when 'blessed' then args[0].is_a?(Ref) ? args[0].blessed.to_s : ''
112
+ when 'reftype' then args[0].is_a?(Ref) ? args[0].kind : ''
113
+ when 'refaddr' then args[0].is_a?(Ref) ? args[0].target.object_id : nil
114
+ when 'weaken' then args[0].is_a?(Ref) ? args[0].weaken! : nil
115
+ when 'dualvar' then Dualvar.new(Conv.to_num(args[0]), Conv.to_str(args[1]))
116
+ when 'looks_like_number' then Conv::NUM_RE.match?(Conv.to_str(args[0])) ? 1 : ''
117
+ when 'floor', 'ceil' then Conv.to_num(args[0]).public_send(@name)
118
+ when 'fmod' then Conv.to_num(args[0]) % Conv.to_num(args[1])
119
+ when 'strftime' then posix_strftime(args)
120
+ when 'INT_MAX' then (2**31) - 1
121
+ when 'Dumper' then data_dumper(env, list_values(env))
122
+ when 'basename' then File.basename(Conv.to_str(args[0]))
123
+ when 'dirname' then File.dirname(Conv.to_str(args[0]))
124
+ when 'catfile', 'catdir' then File.join(*args.map { |value| Conv.to_str(value) })
125
+ when 'rel2abs' then File.expand_path(Conv.to_str(args[0]), Conv.to_str(args[1] || Dir.pwd))
126
+ when 'getcwd' then Dir.pwd
127
+ when 'abs_path' then File.realpath(Conv.to_str(args[0]))
128
+ when 'tempfile', 'tempdir' then temporary_value(args, context)
129
+ when 'mkpath' then file_utils.mkdir_p(Conv.to_str(args[0])).last
130
+ when 'rmtree' then file_utils.rm_rf(Conv.to_str(args[0])).length
131
+ when 'encode_json' then json.generate(deep_value(args[0]))
132
+ when 'decode_json' then ruby_value(json.parse(Conv.to_str(args[0])))
133
+ when 'encode' then Conv.to_str(args[1]).encode(Conv.to_str(args[0]))
134
+ when 'decode'
135
+ Conv.to_str(args[1]).dup.force_encoding(Conv.to_str(args[0])).encode(Encoding::UTF_8)
136
+ when 'md5_hex' then digest::MD5.hexdigest(Conv.to_str(args[0]))
137
+ when 'sha1_hex' then digest::SHA1.hexdigest(Conv.to_str(args[0]))
138
+ when 'sha256_hex' then digest::SHA256.hexdigest(Conv.to_str(args[0]))
139
+ when 'dclone' then ruby_value(deep_value(args[0]))
140
+ when 'carp', 'cluck'
141
+ env.runtime.emit_warning(Conv.to_str(args[0]), file: @file, line: @line)
142
+ 1
143
+ when 'croak', 'confess', 'die' then env.runtime.die(list_values(env), file: @file, line: @line)
144
+ when 'hex' then Conv.to_str(args[0]).to_i(16)
145
+ when 'oct' then Conv.to_str(args[0]).to_i(8)
146
+ when 'rand' then rand(args[0] ? Conv.to_num(args[0]) : 1.0)
147
+ when 'srand' then seed_random(args[0])
148
+ when 'time' then Time.now.to_i
149
+ when 'gettimeofday' then high_resolution_time(context)
150
+ when 'localtime', 'gmtime' then time_parts(args[0], utc: @name == 'gmtime', context:)
151
+ when 'sleep' then sleep(Conv.to_num(args[0] || 0))
152
+ when 'times' then process_times(context)
153
+ when 'pack' then list_values(env).drop(1).pack(Conv.to_str(args[0]))
154
+ when 'unpack' then unpack_values(args, context)
155
+ when 'keys', 'values' then hash_values(env, @name, context)
156
+ when 'each' then hash_each(env, context)
157
+ when 'sort' then list_values(env).sort_by { |value| Conv.to_str(value) }
158
+ when 'pop', 'shift' then array_remove(env, @name)
159
+ when 'unshift' then array_unshift(env)
160
+ when 'splice' then array_splice(env, context)
161
+ when 'chomp', 'chop' then trim(env, @name)
162
+ when 'open' then open_handle(env, args)
163
+ when 'close' then handle(env, @arguments.first, args[0])&.close || ''
164
+ when 'binmode' then binmode(env, args)
165
+ when 'seek'
166
+ handle(env, @arguments.first, args[0]).io.seek(Conv.to_num(args[1]), Conv.to_num(args[2]).to_i) && 1
167
+ when 'tell' then handle(env, @arguments.first, args[0]).io.tell
168
+ when 'fileno' then handle(env, @arguments.first, args[0]).io.fileno
169
+ when 'eof' then eof_value(env, args)
170
+ when 'select' then select_output(env, args)
171
+ when 'opendir' then open_directory(env, args)
172
+ when 'readdir' then read_directory(env, args, context)
173
+ when 'closedir' then directory_handle(env, args).close
174
+ when 'rewinddir' then directory_handle(env, args).rewind
175
+ when 'stat', 'lstat' then stat_values(env, args, context)
176
+ when 'chmod' then File.chmod(Conv.to_num(args.shift).to_i, *args.map { |value| Conv.to_str(value) })
177
+ when 'chown' then File.chown(Conv.to_num(args.shift).to_i, Conv.to_num(args.shift).to_i, *args.map(&:to_s))
178
+ when 'utime' then file_utime(args)
179
+ when 'flock' then handle(env, @arguments.first, args[0]).io.flock(Conv.to_num(args[1]).to_i) ? 1 : ''
180
+ when 'glob' then glob_values(args, context)
181
+ when 'mkdir', 'rmdir', 'unlink', 'truncate' then file_operation(args)
182
+ when 'rename', 'link', 'symlink' then File.public_send(@name, *args.map { |value| Conv.to_str(value) }) && 1
183
+ when 'readlink' then File.readlink(Conv.to_str(args[0]))
184
+ when 'system' then run_system(env)
185
+ when 'exec' then Process.exec(*list_values(env).map { |value| Conv.to_str(value) })
186
+ when 'fork' then fork_process(env)
187
+ when 'wait', 'waitpid' then wait_process(env, args)
188
+ when 'kill' then Process.kill(args.shift, *args.map { |value| Conv.to_num(value).to_i })
189
+ when 'exit'
190
+ status = Conv.to_num(args[0] || 0).to_i
191
+ env.fetch(:scalar, '?').set(status)
192
+ raise PerlExit, status
193
+ when 'getppid' then Process.ppid
194
+ when 'GetOptions' then get_options(env)
195
+ when 'O_RDONLY', 'O_WRONLY', 'O_RDWR', 'O_CREAT', 'O_TRUNC' then file_constant(@name)
196
+ when 'SEEK_SET', 'SEEK_CUR', 'SEEK_END' then IO.const_get(@name)
197
+ when 'EACCES', 'ENOENT' then Errno.const_get(@name)::Errno
198
+ when 'warn' then warn_message(env)
199
+ end
200
+ rescue SystemCallError, IOError => e
201
+ env.fetch(:scalar, '!').set(Dualvar.new(e.errno || 0, e.message))
202
+ ''
203
+ end
204
+
205
+ def scalar_values(env) = @arguments.map { |argument| argument.run(env, :scalar) }
206
+ def list_values(env) = @arguments.flat_map { |argument| argument.run(env, :list) }.map(&:get)
207
+
208
+ def file_utils
209
+ require 'fileutils'
210
+ FileUtils
211
+ end
212
+
213
+ def json
214
+ require 'json'
215
+ JSON
216
+ end
217
+
218
+ def digest
219
+ require 'digest'
220
+ Digest
221
+ end
222
+
223
+ def equal?(left, right)
224
+ return Conv.to_num(left) == Conv.to_num(right) if left.is_a?(Numeric) || right.is_a?(Numeric)
225
+
226
+ Conv.to_str(left) == Conv.to_str(right)
227
+ end
228
+
229
+ def list_sum(env)
230
+ values = list_values(env)
231
+ return nil if values.empty? && @name == 'sum'
232
+
233
+ values.sum { |value| Conv.to_num(value) }
234
+ end
235
+
236
+ def overloaded_abs(env, value)
237
+ found, overloaded = env.runtime.overloaded(value, 'abs')
238
+ found ? overloaded : Conv.to_num(value).abs
239
+ end
240
+
241
+ def list_utility_block(env, context)
242
+ code = @arguments.first&.run(env, :scalar)
243
+ code = code.target if code.is_a?(Ref) && code.kind == 'CODE'
244
+ raise PerlError, "#{@name} requires a code reference" unless code.is_a?(Code)
245
+
246
+ cells = @arguments.drop(1).flat_map { |argument| argument.argument_cells(env) }
247
+ return reduce_cells(env, code, cells) if @name == 'reduce'
248
+
249
+ matches = cells.select { |cell| predicate(env, code, cell) }
250
+ return matches.first&.get if @name == 'first'
251
+
252
+ results = { 'any' => !matches.empty?, 'all' => matches.length == cells.length, 'none' => matches.empty? }
253
+ result = results.fetch(@name)
254
+ value = result ? 1 : ''
255
+ context == :list ? [Scalar.new(value)] : value
256
+ end
257
+
258
+ def predicate(env, code, cell)
259
+ code.environment.with_scope do
260
+ code.environment.bind(:scalar, '_', cell)
261
+ Conv.truthy?(env.runtime.call(code, [], :scalar))
262
+ end
263
+ end
264
+
265
+ def reduce_cells(env, code, cells)
266
+ return nil if cells.empty?
267
+
268
+ a = env.runtime.stash.glob("#{env.package}::a").scalar
269
+ b = env.runtime.stash.glob("#{env.package}::b").scalar
270
+ previous = [a.get, b.get]
271
+ result = cells.first.get
272
+ cells.drop(1).each do |cell|
273
+ a.set(result)
274
+ b.set(cell.get)
275
+ result = env.runtime.call(code, [], :scalar)
276
+ end
277
+ result
278
+ ensure
279
+ a&.set(previous&.first)
280
+ b&.set(previous&.last)
281
+ end
282
+
283
+ def pair_reference(pair)
284
+ Ref.new(PerlArray.new(pair.map { |value| Scalar.new(value) }))
285
+ end
286
+
287
+ def high_resolution_time(context)
288
+ now = Time.now
289
+ context == :list ? [now.to_i, now.usec] : now.to_f
290
+ end
291
+
292
+ def temporary_value(args, context)
293
+ if @name == 'tempdir'
294
+ require 'tmpdir'
295
+ return Dir.mktmpdir(Conv.to_str(args[0] || 'peruby'))
296
+ end
297
+
298
+ require 'tempfile'
299
+ file = Tempfile.new(Conv.to_str(args[0] || 'peruby'))
300
+ values = [IOHandle.new(file), file.path]
301
+ context == :list ? values : values.first
302
+ end
303
+
304
+ def file_constant(name)
305
+ require 'fcntl'
306
+ Fcntl.const_get(name)
307
+ end
308
+
309
+ def get_options(env)
310
+ specifications = list_values(env).each_slice(2).to_h
311
+ cells = env.fetch(:array, 'ARGV').cells
312
+ index = 0
313
+ while index < cells.length
314
+ consumed = consume_option(cells, index, specifications)
315
+ consumed ? cells.slice!(index, consumed) : index += 1
316
+ end
317
+ 1
318
+ end
319
+
320
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
321
+ def consume_option(cells, index, specifications)
322
+ argument = Conv.to_str(cells[index].get)
323
+ return unless argument.start_with?('--')
324
+
325
+ option, inline = argument.delete_prefix('--').split('=', 2)
326
+ spec, target = specifications.find do |name, _value|
327
+ name.to_s.sub(/[=!:].*\z/, '').split('|').include?(option.delete_prefix('no-'))
328
+ end
329
+ return unless spec && target.is_a?(Ref) && target.target.is_a?(Scalar)
330
+
331
+ takes_value = spec.to_s.match?(/[=:][sif]\z/)
332
+ following = cells[index + 1]&.get if takes_value && inline.nil?
333
+ return if takes_value && inline.nil? && following.nil?
334
+
335
+ value = inline || following || (option.start_with?('no-') ? '' : 1)
336
+ value = Conv.to_num(value).to_i if spec.to_s.end_with?('=i', ':i')
337
+ value = Conv.to_num(value) if spec.to_s.end_with?('=f', ':f')
338
+ target.target.set(value)
339
+ takes_value && inline.nil? ? 2 : 1
340
+ end
341
+
342
+ def posix_strftime(args)
343
+ format = Conv.to_str(args[0])
344
+ return Time.at(Conv.to_num(args[1] || Time.now.to_i)).strftime(format) if args.length < 7
345
+
346
+ second, minute, hour, day, month, year = args.drop(1).first(6).map { |value| Conv.to_num(value).to_i }
347
+ Time.local(year + 1900, month + 1, day, hour, minute, second).strftime(format)
348
+ end
349
+
350
+ def data_dumper(env, values)
351
+ terse = Conv.truthy?(env.fetch(:scalar, 'Data::Dumper::Terse').get)
352
+ seen = {}.compare_by_identity
353
+ values.each_with_index.map do |value, index|
354
+ label = "$VAR#{index + 1}"
355
+ prefix = terse ? '' : "$VAR#{index + 1} = "
356
+ suffix = terse ? "\n" : ";\n"
357
+ "#{prefix}#{dump_value(value, prefix.length, seen, label)}#{suffix}"
358
+ end.join
359
+ end
360
+
361
+ def dump_value(value, indent, seen, label)
362
+ target = value.is_a?(Ref) ? value.target : value
363
+ if target.is_a?(PerlArray) || target.is_a?(PerlHash)
364
+ return seen[target] if seen.key?(target)
365
+
366
+ seen[target] = label
367
+ end
368
+ dumped = if target.is_a?(PerlArray)
369
+ dump_array(target, indent, seen, label)
370
+ elsif target.is_a?(PerlHash)
371
+ dump_hash(target, indent, seen, label)
372
+ else
373
+ dump_scalar(target)
374
+ end
375
+ value.is_a?(Ref) && value.blessed ? "bless( #{dumped}, '#{value.blessed}' )" : dumped
376
+ end
377
+
378
+ def dump_array(array, indent, seen, label)
379
+ values = array.cells.each_with_index.map do |cell, index|
380
+ dump_value(cell&.value, indent + 2, seen, "#{label}->[#{index}]")
381
+ end
382
+ return '[]' if values.empty?
383
+
384
+ "[\n#{values.map { |value| "#{' ' * (indent + 2)}#{value}" }.join(",\n")}\n#{' ' * indent}]"
385
+ end
386
+
387
+ def dump_hash(hash, indent, seen, label)
388
+ values = hash.cells.map do |key, cell|
389
+ quoted = dump_scalar(key)
390
+ value = dump_value(cell.value, indent + quoted.length + 6, seen, "#{label}->{#{quoted}}")
391
+ "#{' ' * (indent + 2)}#{quoted} => #{value}"
392
+ end
393
+ return '{}' if values.empty?
394
+
395
+ "{\n#{values.join(",\n")}\n#{' ' * indent}}"
396
+ end
397
+
398
+ def dump_scalar(value)
399
+ return 'undef' if value.nil?
400
+ return Conv.to_str(value) if value.is_a?(Numeric)
401
+
402
+ "'#{Conv.to_str(value).gsub('\\', '\\\\').gsub("'", "\\\\'")}'"
403
+ end
404
+
405
+ def like?(value, pattern)
406
+ regexp = pattern.is_a?(Ref) ? pattern.target : pattern
407
+ regexp.is_a?(Regexp) && regexp.match?(Conv.to_str(value))
408
+ end
409
+
410
+ def deep_value(value)
411
+ if value.is_a?(Ref) && value.target.is_a?(PerlArray)
412
+ return value.target.cells.map { |cell| deep_value(cell.get) }
413
+ end
414
+ if value.is_a?(Ref) && value.target.is_a?(PerlHash)
415
+ return value.target.cells.to_h { |key, cell| [key, deep_value(cell.get)] }
416
+ end
417
+
418
+ value
419
+ end
420
+
421
+ def subtest(env, args)
422
+ code = args[1]
423
+ code = code.target if code.is_a?(Ref) && code.kind == 'CODE'
424
+ raise PerlError, 'subtest requires a code reference' unless code.is_a?(Code)
425
+
426
+ env.runtime.test_builder.subtest(Conv.to_str(args[0])) { env.runtime.call(code, [], :void) }
427
+ end
428
+
429
+ def ruby_value(value)
430
+ case value
431
+ when Array then Ref.new(PerlArray.new(value.map { |item| Scalar.new(ruby_value(item)) }))
432
+ when Hash
433
+ hash = PerlHash.new
434
+ value.each { |key, item| hash.slot(key).set(ruby_value(item)) }
435
+ Ref.new(hash)
436
+ else value
437
+ end
438
+ end
439
+
440
+ def substring(env, args)
441
+ string = Conv.to_str(args[0])
442
+ offset = Conv.to_num(args[1]).to_i
443
+ length = args[2] && Conv.to_num(args[2]).to_i
444
+ value = length ? string.slice(offset, length) : string.slice(offset..)
445
+ return value unless args.length > 3
446
+
447
+ cell = @arguments.first.lvalue(env, :scalar)
448
+ string = string.dup
449
+ string[offset, length] = Conv.to_str(args[3])
450
+ cell.set(string)
451
+ value
452
+ end
453
+
454
+ def string_index(args)
455
+ string = Conv.to_str(args[0])
456
+ needle = Conv.to_str(args[1])
457
+ offset = args[2] && Conv.to_num(args[2]).to_i
458
+ (offset ? string.public_send(@name, needle, offset) : string.public_send(@name, needle)) || -1
459
+ end
460
+
461
+ def warn_message(env)
462
+ message = list_values(env).map { |value| Conv.to_str(value) }.join
463
+ env.runtime.stderr.print(env.runtime.error_message(message))
464
+ 1
465
+ end
466
+
467
+ def reverse(env, context)
468
+ values = list_values(env)
469
+ return values.reverse.map { |value| Scalar.new(value) } if context == :list
470
+
471
+ values.map { |value| Conv.to_str(value) }.join.reverse
472
+ end
473
+
474
+ def time_parts(value, utc:, context:)
475
+ time = Time.at(Conv.to_num(value || Time.now.to_i))
476
+ time = time.utc if utc
477
+ return time.strftime('%a %b %e %H:%M:%S %Y') unless context == :list
478
+
479
+ daylight = time.dst? ? 1 : 0
480
+ [time.sec, time.min, time.hour, time.day, time.month - 1, time.year - 1900,
481
+ time.wday, time.yday - 1, daylight]
482
+ end
483
+
484
+ def seed_random(value)
485
+ seed = value.nil? ? Random.new_seed : Conv.to_num(value).to_i
486
+ Kernel.srand(seed)
487
+ seed
488
+ end
489
+
490
+ def process_times(context)
491
+ times = Process.times
492
+ values = [times.utime, times.stime, times.cutime, times.cstime]
493
+ context == :list ? values : values.first
494
+ end
495
+
496
+ def unpack_values(args, context)
497
+ values = Conv.to_str(args[1]).unpack(Conv.to_str(args[0]))
498
+ context == :list ? values : values.first
499
+ end
500
+
501
+ def run_system(env)
502
+ result = Kernel.system(*list_values(env).map { |value| Conv.to_str(value) })
503
+ status = result.nil? ? -1 : $CHILD_STATUS.to_i
504
+ env.fetch(:scalar, '?').set(status)
505
+ status
506
+ end
507
+
508
+ def fork_process(env)
509
+ pid = Process.fork
510
+ env.fetch(:scalar, '$').set(Process.pid) unless pid
511
+ pid || 0
512
+ end
513
+
514
+ def wait_process(env, args)
515
+ pid = if @name == 'wait'
516
+ Process.wait
517
+ else
518
+ Process.waitpid(Conv.to_num(args[0]).to_i, Conv.to_num(args[1] || 0).to_i)
519
+ end
520
+ env.fetch(:scalar, '?').set($CHILD_STATUS.to_i)
521
+ pid
522
+ rescue Errno::ECHILD
523
+ -1
524
+ end
525
+
526
+ def hash_values(env, name, context)
527
+ hash = @arguments.first.lvalue(env, :scalar)
528
+ raise PerlError, "Type of arg 1 to #{name} must be hash" unless hash.is_a?(PerlHash)
529
+
530
+ values = name == 'keys' ? hash.keys : hash.keys.map { |key| hash.fetch(key) }
531
+ context == :list ? values : values.length
532
+ end
533
+
534
+ def hash_each(env, context)
535
+ hash = @arguments.first.lvalue(env, :scalar)
536
+ raise PerlError, 'Type of arg 1 to each must be hash' unless hash.is_a?(PerlHash)
537
+
538
+ pair = hash.each_pair
539
+ return context == :list ? [] : nil unless pair
540
+
541
+ key, cell = pair
542
+ context == :list ? [Scalar.new(key), cell] : key
543
+ end
544
+
545
+ def array_remove(env, name)
546
+ array = if @arguments.empty?
547
+ env.fetch(:array, env.state_owner ? '_' : 'ARGV')
548
+ else
549
+ @arguments.first.lvalue(env, :scalar)
550
+ end
551
+ cell = name == 'pop' ? array.cells.pop : array.cells.shift
552
+ cell&.get
553
+ end
554
+
555
+ def array_unshift(env)
556
+ array = @arguments.first.lvalue(env, :scalar)
557
+ array.cells.unshift(*@arguments.drop(1).flat_map { |arg| arg.argument_cells(env) }.map(&:copy))
558
+ array.size
559
+ end
560
+
561
+ # rubocop:disable-next Metrics/AbcSize
562
+ def array_splice(env, context)
563
+ array = @arguments.first.lvalue(env, :scalar)
564
+ args = scalar_values(env)
565
+ offset = Conv.to_num(args[1] || 0).to_i
566
+ offset += array.size if offset.negative?
567
+ length = args[2] ? Conv.to_num(args[2]).to_i : array.size - offset
568
+ length = array.size - offset + length if length.negative?
569
+ replacement = @arguments.drop(3).flat_map { |arg| arg.argument_cells(env) }.map(&:copy)
570
+ removed = array.cells.slice!(offset, length) || []
571
+ array.cells.insert(offset, *replacement)
572
+ context == :list ? removed : removed.last&.get
573
+ end
574
+
575
+ def trim(env, name)
576
+ cell = @arguments.first.lvalue(env, :scalar)
577
+ string = Conv.to_str(cell.get)
578
+ removed = if name == 'chomp'
579
+ string.sub!(/\R\z/, '') ? 1 : 0
580
+ else
581
+ string.slice!(-1).to_s.length
582
+ end
583
+ cell.set(string)
584
+ removed
585
+ end
586
+
587
+ def open_handle(env, args)
588
+ target = @arguments.first
589
+ values = @arguments.drop(1).flat_map { |argument| argument.run(env, :list) }.map(&:get)
590
+ marker, layers, source = open_spec(values)
591
+ io = open_io(marker, source, values.drop(2))
592
+ apply_layers(io, layers)
593
+ assign_handle(env, target, args[0], IOHandle.new(io, layers:))
594
+ 1
595
+ end
596
+
597
+ def open_spec(values)
598
+ if values.length == 1
599
+ source = Conv.to_str(values[0])
600
+ marker = source.slice!(/\A(?:\+?[<>]{1,2}|-\||\|-)/) || '<'
601
+ marker, layers = marker.split(':', 2)
602
+ return [marker, layers, source]
603
+ end
604
+ marker, layers = Conv.to_str(values[0]).split(':', 2)
605
+ [marker, layers, values[1]]
606
+ end
607
+
608
+ def ruby_mode(marker)
609
+ { '<' => 'r', '>' => 'w', '>>' => 'a', '+<' => 'r+', '+>' => 'w+', '+>>' => 'a+' }.fetch(marker, marker)
610
+ end
611
+
612
+ def open_io(marker, source, command_tail)
613
+ if %w[-| |-].include?(marker)
614
+ command = [Conv.to_str(source), *command_tail.map { |item| Conv.to_str(item) }]
615
+ return IO.popen(command, marker == '-|' ? 'r' : 'w')
616
+ end
617
+
618
+ if source.is_a?(Ref) && source.target.is_a?(Scalar)
619
+ return StringIO.new(Conv.to_str(source.target.get), ruby_mode(marker))
620
+ end
621
+
622
+ File.open(Conv.to_str(source), ruby_mode(marker))
623
+ end
624
+
625
+ def apply_layers(io, layers)
626
+ return unless layers
627
+
628
+ encoding = layers[/encoding\(([^)]+)\)/, 1] || ('UTF-8' if layers == 'utf8')
629
+ io.set_encoding(encoding) if encoding && io.respond_to?(:set_encoding)
630
+ io.binmode if layers == 'raw'
631
+ end
632
+
633
+ def assign_handle(env, operation, value, io_handle)
634
+ if operation.respond_to?(:lvalue)
635
+ operation.lvalue(env, :scalar).set(io_handle)
636
+ else
637
+ env.runtime.stash.glob(Conv.to_str(value)).io = io_handle
638
+ end
639
+ rescue PerlError
640
+ env.runtime.stash.glob(Conv.to_str(value)).io = io_handle
641
+ end
642
+
643
+ def handle(env, operation, value)
644
+ candidate = operation&.run(env, :scalar)
645
+ return candidate if candidate.is_a?(IOHandle)
646
+
647
+ env.runtime.stash.glob(Conv.to_str(value)).io
648
+ end
649
+
650
+ def binmode(env, args)
651
+ io_handle = handle(env, @arguments.first, args[0])
652
+ layer = Conv.to_str(args[1] || ':raw')
653
+ apply_layers(io_handle.io, layer.delete_prefix(':'))
654
+ io_handle.layers << layer
655
+ 1
656
+ end
657
+
658
+ def eof_value(env, args)
659
+ io_handle = args.empty? ? env.runtime.last_input_handle : handle(env, @arguments.first, args[0])
660
+ io_handle ||= env.runtime.stash.glob('STDIN').io
661
+ io_handle.io.eof? ? 1 : ''
662
+ end
663
+
664
+ def select_output(env, args)
665
+ io_handle = args.empty? ? nil : handle(env, @arguments.first, args[0])
666
+ env.runtime.select_output(io_handle)
667
+ end
668
+
669
+ def open_directory(env, args)
670
+ assign_handle(env, @arguments.first, args[0], DirectoryHandle.new(Conv.to_str(args[1])))
671
+ 1
672
+ end
673
+
674
+ def directory_handle(env, args)
675
+ value = @arguments.first&.run(env, :scalar)
676
+ return value if value.is_a?(DirectoryHandle)
677
+
678
+ handle = env.runtime.stash.glob(Conv.to_str(args[0])).io
679
+ raise PerlError, 'readdir() attempted on invalid dirhandle' unless handle.is_a?(DirectoryHandle)
680
+
681
+ handle
682
+ end
683
+
684
+ def read_directory(env, args, context)
685
+ directory = directory_handle(env, args)
686
+ return directory.read if context != :list
687
+
688
+ entries = []
689
+ loop do
690
+ entry = directory.read
691
+ break unless entry
692
+
693
+ entries << entry
694
+ end
695
+ entries
696
+ end
697
+
698
+ def stat_values(env, args, context)
699
+ stat = env.runtime.file_stat(Conv.to_str(args[0]), lstat: @name == 'lstat')
700
+ return 1 unless context == :list
701
+
702
+ [stat.dev, stat.ino, stat.mode, stat.nlink, stat.uid, stat.gid, stat.rdev, stat.size,
703
+ stat.atime.to_i, stat.mtime.to_i, stat.ctime.to_i, stat.blksize, stat.blocks]
704
+ end
705
+
706
+ def file_utime(args)
707
+ access = Time.at(Conv.to_num(args.shift))
708
+ modify = Time.at(Conv.to_num(args.shift))
709
+ File.utime(access, modify, *args.map { |value| Conv.to_str(value) })
710
+ end
711
+
712
+ def glob_values(args, context)
713
+ return args.flat_map { |pattern| Dir.glob(Conv.to_str(pattern)) } if context == :list
714
+
715
+ @glob_values = Dir.glob(Conv.to_str(args[0])) if @glob_values.nil? || @glob_values.empty?
716
+ @glob_values.shift
717
+ end
718
+
719
+ def file_operation(args)
720
+ case @name
721
+ when 'mkdir' then Dir.mkdir(Conv.to_str(args[0]), Conv.to_num(args[1] || 0o777).to_i)
722
+ when 'rmdir' then Dir.rmdir(Conv.to_str(args[0]))
723
+ when 'unlink' then File.unlink(*args.map { |value| Conv.to_str(value) })
724
+ when 'truncate' then File.truncate(Conv.to_str(args[0]), Conv.to_num(args[1]).to_i)
725
+ end
726
+ end
727
+
728
+ # Mutable view used when substr appears on the left of an assignment.
729
+ class SubstringCell
730
+ def initialize(cell, offset, length)
731
+ @cell = cell
732
+ @offset = offset
733
+ @length = length
734
+ end
735
+
736
+ def get
737
+ string = Conv.to_str(@cell.get)
738
+ @length ? string.slice(@offset, @length) : string.slice(@offset..)
739
+ end
740
+
741
+ def set(value)
742
+ string = Conv.to_str(@cell.get).dup
743
+ @length ? string[@offset, @length] = Conv.to_str(value) : string[@offset..] = Conv.to_str(value)
744
+ @cell.set(string)
745
+ self
746
+ end
747
+ end
748
+ end
749
+
750
+ # Perl file-test operators backed by File::Stat predicates.
751
+ class FileTest < Base
752
+ def initialize(operator, expression)
753
+ @operator = operator.delete_prefix('-')
754
+ @expression = expression
755
+ end
756
+
757
+ def run(env, context)
758
+ path = Conv.to_str(@expression.run(env, :scalar))
759
+ result = test(path, env)
760
+ context == :list ? [Scalar.new(result)] : result
761
+ rescue SystemCallError
762
+ context == :list ? [Scalar.new('')] : ''
763
+ end
764
+
765
+ private
766
+
767
+ # rubocop:disable-next Metrics/AbcSize
768
+ def test(path, env)
769
+ return tty(path, env) if @operator == 't'
770
+
771
+ stat = env.runtime.file_stat(path, lstat: @operator == 'l')
772
+ actual_path = path == '_' ? env.runtime.file_stat_path : path
773
+ predicates = { 'e' => true, 'f' => stat.file?, 'd' => stat.directory?, 'l' => stat.symlink?,
774
+ 'r' => File.readable?(actual_path), 'w' => File.writable?(actual_path),
775
+ 'x' => File.executable?(actual_path), 'z' => stat.zero?, 'p' => stat.pipe?, 'S' => stat.socket?,
776
+ 'b' => stat.blockdev?, 'c' => stat.chardev?, 'u' => stat.setuid?, 'g' => stat.setgid?,
777
+ 'k' => stat.sticky? }
778
+ return stat.size if @operator == 's'
779
+ return text_file?(actual_path) == (@operator == 'T') ? 1 : '' if %w[T B].include?(@operator)
780
+ return predicates.fetch(@operator) ? 1 : '' if predicates.key?(@operator)
781
+
782
+ return (Time.now - stat.mtime) / 86_400 if @operator == 'M'
783
+ return (Time.now - stat.atime) / 86_400 if @operator == 'A'
784
+ return (Time.now - stat.ctime) / 86_400 if @operator == 'C'
785
+
786
+ ''
787
+ end
788
+
789
+ def tty(path, env)
790
+ handle = env.runtime.stash.glob(path).io
791
+ io = handle&.io
792
+ io&.tty? ? 1 : ''
793
+ end
794
+
795
+ def text_file?(path)
796
+ bytes = File.binread(path, 1024).bytes
797
+ return true if bytes.empty?
798
+ return false if bytes.include?(0)
799
+
800
+ controls = bytes.count { |byte| byte < 32 && ![9, 10, 12, 13].include?(byte) }
801
+ controls.fdiv(bytes.length) < 0.3
802
+ end
803
+ end
804
+
805
+ # Compile-time pragmas and registered core modules; loading expands in P12.
806
+ class Use < Base
807
+ CORE = ModuleLoader::INTERNAL_MODULES
808
+
809
+ def initialize(module_name, imports, disable, package)
810
+ @module_name = module_name
811
+ @imports = imports
812
+ @disable = disable
813
+ @package = package
814
+ end
815
+
816
+ def run(env, context) # rubocop:disable Metrics/AbcSize
817
+ imports, version, suppress = options
818
+ env.configure_strict(imports, disable: @disable) if @module_name == 'strict'
819
+ env.runtime.configure_warnings(imports, disable: @disable) if @module_name == 'warnings'
820
+ configure_inc(env, imports) if @module_name == 'lib' && !@disable
821
+ env.runtime.module_loader.require_module(@module_name) unless @disable
822
+ configure_inheritance(env, imports) if %w[parent base].include?(@module_name) && !@disable
823
+ configure_overload(env, imports) if @module_name == 'overload'
824
+ check_version(env, version) if version
825
+ invoke_import(env, imports, suppress)
826
+ alias_imports(env, imports) unless @disable || suppress
827
+ if %w[Test::More Test::Simple].include?(@module_name) && imports.first == 'tests'
828
+ env.runtime.test_builder.plan(imports.last)
829
+ end
830
+ context == :list ? [Scalar.new(1)] : 1
831
+ end
832
+
833
+ def warning_directive? = @module_name == 'warnings'
834
+
835
+ private
836
+
837
+ def options
838
+ imports = @imports.dup
839
+ suppress = imports.delete('__NO_IMPORT__')
840
+ version = imports.shift(2).last if imports.first == '__VERSION__'
841
+ [imports, version, suppress]
842
+ end
843
+
844
+ def check_version(env, required)
845
+ actual = env.runtime.stash.glob("#{@module_name}::VERSION").scalar.get
846
+ return if actual && Conv.to_num(actual) >= Conv.to_num(required)
847
+
848
+ raise PerlError, "#{@module_name} version #{required} required"
849
+ end
850
+
851
+ def invoke_import(env, imports, suppress)
852
+ return if suppress
853
+
854
+ method = @disable ? 'unimport' : 'import'
855
+ code = env.runtime.stash.fetch("#{@module_name}::#{method}")&.code
856
+ return unless code
857
+
858
+ arguments = [Scalar.new(@module_name), *imports.map { |name| Scalar.new(name) }]
859
+ env.runtime.call(code, arguments, :void)
860
+ end
861
+
862
+ def alias_imports(env, imports)
863
+ names = if imports.empty?
864
+ env.runtime.stash.glob("#{@module_name}::EXPORT").array.cells.map(&:get)
865
+ else
866
+ imports
867
+ end
868
+ names.each do |name|
869
+ code = env.runtime.stash.fetch("#{@module_name}::#{name}")&.code
870
+ env.runtime.stash.glob("#{@package}::#{name}").code = code if code
871
+ end
872
+ env.runtime.stash.touch unless names.empty?
873
+ end
874
+
875
+ def configure_inc(env, imports)
876
+ imports.reverse_each { |path| env.fetch(:array, 'INC').cells.unshift(Scalar.new(path)) }
877
+ end
878
+
879
+ def configure_inheritance(env, imports)
880
+ parents = imports.reject { |name| name == '-norequire' }
881
+ parents.each { |parent| env.runtime.module_loader.require_module(parent) } unless imports.include?('-norequire')
882
+ env.runtime.stash.glob("#{@package}::ISA").array.cells.concat(parents.map { |name| Scalar.new(name) })
883
+ env.runtime.stash.touch
884
+ end
885
+
886
+ def configure_overload(env, imports)
887
+ env.runtime.register_overload(@package, imports.each_slice(2).to_h, disable: @disable)
888
+ end
889
+ end
890
+
891
+ # Runtime require expression.
892
+ class Require < Base
893
+ def initialize(expression)
894
+ @expression = expression
895
+ end
896
+
897
+ def run(env, context)
898
+ result = env.runtime.module_loader.require_module(@expression.run(env, :scalar))
899
+ context == :list ? [Scalar.new(result)] : result
900
+ end
901
+ end
902
+ end
903
+ end