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,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ module Op
5
+ # Array/hash access over package aggregates or references, with autovivification.
6
+ class Element < Base
7
+ def initialize(container, key, kind, dereference, **options)
8
+ @container = container
9
+ @key = key
10
+ @kind = kind
11
+ @dereference = dereference
12
+ @slice = options.fetch(:slice, false)
13
+ @key_value = options.fetch(:key_value, false)
14
+ end
15
+
16
+ def run(env, context)
17
+ if @key_value && context == :list
18
+ container = aggregate(env)
19
+ return keys(env).flat_map { |key| [Scalar.new(Conv.to_str(key)), slot(container, key)] }
20
+ end
21
+
22
+ cells = lvalue(env, @slice ? :list : :scalar)
23
+ return cells if context == :list && cells.is_a?(Array)
24
+
25
+ cell = cells.is_a?(Array) ? cells.last : cells
26
+ context == :list ? [cell] : cell&.get
27
+ end
28
+
29
+ def to_callable
30
+ return super if @slice || @key_value
31
+
32
+ key = @key.to_callable
33
+ lvalue = lvalue_callable
34
+ lambda do |env, context|
35
+ cell = lvalue.call(env, key.call(env, :scalar))
36
+ context == :list ? [cell] : cell&.value
37
+ end
38
+ end
39
+
40
+ def scalar_callable
41
+ return unless !@slice && !@key_value
42
+
43
+ key = @key.scalar_callable if @key.respond_to?(:scalar_callable)
44
+ key ||= ->(env) { @key.run(env, :scalar) }
45
+ lvalue = lvalue_callable
46
+ ->(env) { lvalue.call(env, key.call(env))&.value }
47
+ end
48
+
49
+ def lvalue_callable
50
+ return ->(env, _key = nil) { lvalue(env, :list) } if @slice
51
+
52
+ container = aggregate_callable
53
+ kind = @kind
54
+ lambda do |env, supplied_key = nil|
55
+ key = supplied_key || @key.run(env, :scalar)
56
+ key = key.value if key.respond_to?(:value)
57
+ aggregate = container.call(env)
58
+ kind == :array ? aggregate.slot(Conv.to_num(key).to_i) : aggregate.slot(Conv.to_str(key))
59
+ end
60
+ end
61
+
62
+ def lvalue(env, context)
63
+ container = aggregate(env)
64
+ unless @slice
65
+ key = @key.run(env, :scalar)
66
+ cell = slot(container, key.respond_to?(:get) ? key.get : key)
67
+ return context == :list ? [cell] : cell
68
+ end
69
+
70
+ keys = @key.run(env, @slice ? :list : :scalar)
71
+ keys = Array(keys).map { |key| key.respond_to?(:get) ? key.get : key }
72
+ cells = keys.map { |key| slot(container, key) }
73
+ context == :list ? cells : cells.last
74
+ end
75
+
76
+ def exists?(env)
77
+ container = aggregate(env)
78
+ keys(env).all? { |key| container.exists?(key) }
79
+ end
80
+
81
+ def delete(env)
82
+ container = aggregate(env)
83
+ keys(env).map { |key| container.delete(key) }
84
+ end
85
+
86
+ def localize(env, stack)
87
+ raise PerlError, "Can't localize slice" if @slice
88
+
89
+ container = aggregate(env)
90
+ key = @key.run(env, :scalar)
91
+ key = key.get if key.respond_to?(:get)
92
+ if @kind == :array
93
+ stack.localize_array(container, Conv.to_num(key).to_i)
94
+ else
95
+ stack.localize_hash(container, Conv.to_str(key))
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def aggregate_callable
102
+ return ->(env) { aggregate(env) } unless @container.respond_to?(:aggregate_callable)
103
+
104
+ @container.aggregate_callable(@kind, dereference: @dereference)
105
+ end
106
+
107
+ def aggregate(env)
108
+ return @container.aggregate(env, @kind, dereference: @dereference) if @container.respond_to?(:aggregate)
109
+
110
+ dereference(@container.lvalue(env, :scalar))
111
+ end
112
+
113
+ def keys(env)
114
+ values = @key.run(env, @slice ? :list : :scalar)
115
+ Array(values).map { |key| key.respond_to?(:get) ? key.get : key }
116
+ end
117
+
118
+ def dereference(cell)
119
+ value = cell.get
120
+ if value.nil?
121
+ target = @kind == :array ? PerlArray.new : PerlHash.new
122
+ value = Ref.new(target)
123
+ cell.set(value)
124
+ end
125
+ expected = @kind == :array ? PerlArray : PerlHash
126
+ raise PerlError, "Not a #{@kind.to_s.upcase} reference" unless value.is_a?(Ref) && value.target.is_a?(expected)
127
+
128
+ value.target
129
+ end
130
+
131
+ def slot(container, key)
132
+ @kind == :array ? container.slot(Conv.to_num(key).to_i) : container.slot(Conv.to_str(key))
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ module Op
5
+ # Unary scalar expression.
6
+ class Unary < Base
7
+ def initialize(operator, expression)
8
+ @operator = operator
9
+ @expression = expression
10
+ end
11
+
12
+ def run(env, context)
13
+ return reference(env, context) if @operator == :reference
14
+
15
+ value = @expression.run(env, :scalar)
16
+ if @operator == :negate && value.is_a?(Ref)
17
+ found, overloaded = env.runtime.overloaded(value, 'neg')
18
+ return context == :list ? [Scalar.new(overloaded)] : overloaded if found
19
+ end
20
+ operations = { negate: -> { -Conv.to_num(value) }, positive: -> { Conv.to_num(value) },
21
+ not: -> { Conv.truthy?(value) ? '' : 1 }, bit_not: -> { Conv.bit_not(value) } }
22
+ result = operations.fetch(@operator).call
23
+ context == :list ? [Scalar.new(result)] : result
24
+ end
25
+
26
+ def reference(env, context)
27
+ return @expression.references(env, context) if @expression.respond_to?(:references)
28
+
29
+ target = begin
30
+ @expression.lvalue(env, :scalar)
31
+ rescue PerlError
32
+ Scalar.new(@expression.run(env, :scalar))
33
+ end
34
+ value = Ref.new(target)
35
+ context == :list ? [Scalar.new(value)] : value
36
+ end
37
+ end
38
+
39
+ # Parenthesized expression, retained for Perl's \(LIST) reference semantics.
40
+ class Group < Base
41
+ def initialize(expression)
42
+ @expression = expression
43
+ end
44
+
45
+ def run(env, context) = @expression.run(env, context)
46
+ def lvalue(env, context) = @expression.lvalue(env, context)
47
+ def argument_cells(env) = @expression.argument_cells(env)
48
+ def aggregate(env, kind) = @expression.aggregate(env, kind)
49
+
50
+ def references(env, context)
51
+ values = @expression.argument_cells(env).map { |cell| Scalar.new(Ref.new(cell)) }
52
+ context == :list ? values : values.last&.get
53
+ end
54
+ end
55
+
56
+ # Post-increment/decrement preserving the old expression value.
57
+ class Postfix < Base
58
+ def initialize(operator, expression)
59
+ @operator = operator
60
+ @expression = expression
61
+ end
62
+
63
+ def run(env, context)
64
+ cell = @expression.lvalue(env, :scalar)
65
+ old = cell.get
66
+ value = @operator == :increment ? Conv.increment(old) : Conv.to_num(old) - 1
67
+ cell.set(value)
68
+ context == :list ? [Scalar.new(old)] : old
69
+ end
70
+ end
71
+
72
+ # Pre-increment/decrement returning the updated value.
73
+ class Prefix < Base
74
+ def initialize(operator, expression)
75
+ @operator = operator
76
+ @expression = expression
77
+ end
78
+
79
+ def run(env, context)
80
+ cell = @expression.lvalue(env, :scalar)
81
+ value = @operator == :increment ? Conv.increment(cell.get) : Conv.to_num(cell.get) - 1
82
+ cell.set(value)
83
+ context == :list ? [cell] : value
84
+ end
85
+ end
86
+
87
+ # Arithmetic, comparison, and short-circuit binary expression.
88
+ class Binary < Base
89
+ OVERLOAD_OPERATORS = {
90
+ :+ => '+', :- => '-', :* => '*', divide: '/', modulo: '%', concat: '.',
91
+ num_eq: '==', num_ne: '==', num_lt: '<=>', num_gt: '<=>', num_le: '<=>', num_ge: '<=>', num_cmp: '<=>',
92
+ str_eq: 'eq', str_ne: 'eq', str_lt: 'cmp', str_gt: 'cmp', str_le: 'cmp', str_ge: 'cmp', str_cmp: 'cmp'
93
+ }.freeze
94
+ NO_OVERLOAD = Object.new.freeze
95
+ OPERATIONS = {
96
+ :+ => ->(left, right) { Conv.norm_int(Conv.to_num(left) + Conv.to_num(right)) },
97
+ :- => ->(left, right) { Conv.norm_int(Conv.to_num(left) - Conv.to_num(right)) },
98
+ :* => ->(left, right) { Conv.norm_int(Conv.to_num(left) * Conv.to_num(right)) },
99
+ divide: ->(left, right) { Conv.divide(left, right) },
100
+ modulo: ->(left, right) { Conv.modulo(left, right) },
101
+ pow: ->(left, right) { Conv.norm_int(Conv.to_num(left)**Conv.to_num(right)) },
102
+ concat: ->(left, right) { Conv.to_str(left) + Conv.to_str(right) },
103
+ repeat: ->(left, right) { Conv.repeat(left, right) },
104
+ left_shift: ->(left, right) { Conv.to_num(left).to_i << Conv.to_num(right).to_i },
105
+ right_shift: ->(left, right) { Conv.to_num(left).to_i >> Conv.to_num(right).to_i },
106
+ bit_and: ->(left, right) { Conv.bitwise(:&, left, right) },
107
+ bit_or: ->(left, right) { Conv.bitwise(:|, left, right) },
108
+ bit_xor: ->(left, right) { Conv.bitwise(:^, left, right) }
109
+ }.freeze
110
+
111
+ def initialize(operator, left, right)
112
+ @operator = operator
113
+ @left = left
114
+ @right = right
115
+ @flip_flops = ObjectSpace::WeakMap.new if range?
116
+ end
117
+
118
+ def run(env, context)
119
+ return run_range(env, context) if range?
120
+
121
+ result = if %i[and or defined_or].include?(@operator)
122
+ short_circuit(env)
123
+ elsif @operator == :xor
124
+ Conv.truthy?(@left.run(env, :scalar)) ^ Conv.truthy?(@right.run(env, :scalar)) ? 1 : ''
125
+ else
126
+ calculate(@left.run(env, :scalar), @right.run(env, :scalar), env)
127
+ end
128
+ context == :list ? Array(result).map { |value| Scalar.new(value) } : result
129
+ end
130
+
131
+ def to_callable
132
+ return super if range?
133
+
134
+ scalar = scalar_callable
135
+ lambda do |env, context|
136
+ result = scalar.call(env)
137
+ context == :list ? Array(result).map { |value| Scalar.new(value) } : result
138
+ end
139
+ end
140
+
141
+ def scalar_callable
142
+ return ->(env) { scalar_range(env) } if range?
143
+
144
+ left = child_scalar(@left)
145
+ right = child_scalar(@right)
146
+ return ->(env) { compiled_scalar_short_circuit(env, left, right) } if %i[and or defined_or].include?(@operator)
147
+
148
+ ->(env) { compiled_calculate(left.call(env), right.call(env), env) }
149
+ end
150
+
151
+ private
152
+
153
+ def child_scalar(operation)
154
+ callable = operation.scalar_callable if operation.respond_to?(:scalar_callable)
155
+ callable || ->(env) { operation.run(env, :scalar) }
156
+ end
157
+
158
+ def compiled_scalar_short_circuit(env, left_operation, right_operation)
159
+ left = left_operation.call(env)
160
+ return Conv.truthy?(left) ? right_operation.call(env) : left if @operator == :and
161
+ return Conv.truthy?(left) ? left : right_operation.call(env) if @operator == :or
162
+
163
+ left.nil? ? right_operation.call(env) : left
164
+ end
165
+
166
+ def compiled_calculate(left, right, env)
167
+ return calculate(left, right, env) if env.runtime.overloads?
168
+
169
+ if left.is_a?(Numeric) && right.is_a?(Numeric)
170
+ return Conv.norm_int(left + right) if @operator == :+
171
+ return Conv.norm_int(left - right) if @operator == :-
172
+ return Conv.norm_int(left * right) if @operator == :*
173
+ return Conv.divide(left, right) if @operator == :divide
174
+ return Conv.modulo(left, right) if @operator == :modulo
175
+ return Conv.norm_int(left**right) if @operator == :pow
176
+ return compiled_numeric_comparison(left, right) if @operator.to_s.start_with?('num_')
177
+ end
178
+ calculate(left, right, env)
179
+ end
180
+
181
+ def compiled_numeric_comparison(left, right)
182
+ comparison = left <=> right
183
+ case @operator
184
+ when :num_eq then comparison.zero? ? 1 : ''
185
+ when :num_ne then comparison.zero? ? '' : 1
186
+ when :num_lt then comparison.negative? ? 1 : ''
187
+ when :num_gt then comparison.positive? ? 1 : ''
188
+ when :num_le then comparison <= 0 ? 1 : ''
189
+ when :num_ge then comparison >= 0 ? 1 : ''
190
+ else comparison
191
+ end
192
+ end
193
+
194
+ def short_circuit(env)
195
+ left = @left.run(env, :scalar)
196
+ return Conv.truthy?(left) ? @right.run(env, :scalar) : left if @operator == :and
197
+ return Conv.truthy?(left) ? left : @right.run(env, :scalar) if @operator == :or
198
+
199
+ left.nil? ? @right.run(env, :scalar) : left
200
+ end
201
+
202
+ def compiled_short_circuit(env, left_operation, right_operation)
203
+ left = left_operation.call(env, :scalar)
204
+ return Conv.truthy?(left) ? right_operation.call(env, :scalar) : left if @operator == :and
205
+ return Conv.truthy?(left) ? left : right_operation.call(env, :scalar) if @operator == :or
206
+
207
+ left.nil? ? right_operation.call(env, :scalar) : left
208
+ end
209
+
210
+ def calculate(left, right, env = nil)
211
+ overloaded = overload(left, right, env)
212
+ return overloaded unless overloaded.equal?(NO_OVERLOAD)
213
+
214
+ if OPERATIONS.key?(@operator)
215
+ operation = OPERATIONS.fetch(@operator)
216
+ warnable = !left.is_a?(Numeric) || !right.is_a?(Numeric)
217
+ if numeric_operator? && warnable && env&.runtime&.warnings_active?
218
+ return operation.call(env.runtime.number(left), env.runtime.number(right))
219
+ end
220
+
221
+ return operation.call(left, right)
222
+ end
223
+ return compare(left, right, env) if @operator.to_s.start_with?('num_', 'str_')
224
+ return Conv.truthy?(left) ^ Conv.truthy?(right) ? 1 : '' if @operator == :xor
225
+
226
+ raise PerlError, "Unsupported binary operator #{@operator}"
227
+ end
228
+
229
+ def overload(left, right, env)
230
+ operator = OVERLOAD_OPERATORS[@operator]
231
+ return NO_OVERLOAD unless env && operator && env.runtime.overloads?
232
+
233
+ found, value = env.runtime.overloaded(left, operator, right)
234
+ found, value = env.runtime.overloaded(right, operator, left, swapped: true) unless found
235
+ return NO_OVERLOAD unless found
236
+
237
+ return Conv.truthy?(value) ? '' : 1 if %i[num_ne str_ne].include?(@operator)
238
+
239
+ if @operator.to_s.match?(/_(?:lt|gt|le|ge)\z/)
240
+ relation = @operator.to_s.split('_').last
241
+ comparison = Conv.to_num(value)
242
+ matches = { 'lt' => comparison.negative?, 'gt' => comparison.positive?,
243
+ 'le' => comparison <= 0, 'ge' => comparison >= 0 }
244
+ return matches.fetch(relation) ? 1 : ''
245
+ end
246
+
247
+ value
248
+ end
249
+
250
+ def compare(left, right, env)
251
+ string = @operator.to_s.start_with?('str_')
252
+ comparison = string ? Conv.to_str(left) <=> Conv.to_str(right) : number(left, env) <=> number(right, env)
253
+ relation = @operator.to_s.delete_prefix('num_').delete_prefix('str_')
254
+ relations = { eq: comparison.zero?, ne: !comparison.zero?, lt: comparison.negative?, gt: comparison.positive?,
255
+ le: comparison <= 0, ge: comparison >= 0, cmp: comparison }
256
+ perl_boolean(relations.fetch(relation.to_sym))
257
+ end
258
+
259
+ def numeric_operator?
260
+ !%i[concat repeat].include?(@operator)
261
+ end
262
+
263
+ def number(value, env)
264
+ env&.runtime&.warnings_active? ? env.runtime.number(value) : Conv.to_num(value)
265
+ end
266
+
267
+ def perl_boolean(value)
268
+ return 1 if value == true
269
+ return '' if value == false
270
+
271
+ value
272
+ end
273
+
274
+ def range? = %i[range range_exclusive].include?(@operator)
275
+
276
+ def run_range(env, context)
277
+ result = if context == :list
278
+ range_values(@left.run(env, :scalar), @right.run(env, :scalar))
279
+ else
280
+ scalar_range(env)
281
+ end
282
+ context == :list ? result.map { |value| Scalar.new(value) } : result
283
+ end
284
+
285
+ # perlop: in scalar context .. and ... are stateful flip-flops, not list constructors.
286
+ def scalar_range(env)
287
+ owner = env.state_owner || env.runtime
288
+ count = @flip_flops[owner]
289
+ unless count
290
+ return '' unless Conv.truthy?(@left.run(env, :scalar))
291
+
292
+ count = 1
293
+ return finish_flip_flop(owner, count) if @operator == :range && Conv.truthy?(@right.run(env, :scalar))
294
+
295
+ @flip_flops[owner] = count
296
+ return count
297
+ end
298
+
299
+ count += 1
300
+ return finish_flip_flop(owner, count) if Conv.truthy?(@right.run(env, :scalar))
301
+
302
+ @flip_flops[owner] = count
303
+ count
304
+ end
305
+
306
+ def finish_flip_flop(owner, count)
307
+ @flip_flops[owner] = nil
308
+ "#{count}E0"
309
+ end
310
+
311
+ def range_values(left, right)
312
+ return (Conv.to_str(left)..Conv.to_str(right)).to_a if left.is_a?(String) || right.is_a?(String)
313
+
314
+ (Conv.to_num(left).to_i..Conv.to_num(right).to_i).to_a
315
+ end
316
+ end
317
+
318
+ # Lazy conditional expression.
319
+ class Ternary < Base
320
+ def initialize(condition, true_expression, false_expression)
321
+ @condition = condition
322
+ @true_expression = true_expression
323
+ @false_expression = false_expression
324
+ end
325
+
326
+ def run(env, context)
327
+ branch = Conv.truthy?(@condition.run(env, :scalar)) ? @true_expression : @false_expression
328
+ branch.run(env, context)
329
+ end
330
+
331
+ def to_callable
332
+ condition = @condition.to_callable
333
+ true_expression = @true_expression.to_callable
334
+ false_expression = @false_expression.to_callable
335
+ lambda do |env, context|
336
+ branch = Conv.truthy?(condition.call(env, :scalar)) ? true_expression : false_expression
337
+ branch.call(env, context)
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ module Op
5
+ # Print operation with Perl output separators.
6
+ class Print < Base
7
+ def initialize(handle, arguments)
8
+ @handle = handle
9
+ @arguments = arguments
10
+ end
11
+
12
+ def run(env, context)
13
+ values = @arguments ? @arguments.run(env, :list) : [env.fetch(:scalar, '_')]
14
+ values.each { |cell| env.runtime.warning('uninitialized', 'Use of uninitialized value') if cell.get.nil? }
15
+ separator = Conv.to_str(env.fetch(:scalar, ',').get)
16
+ ending = Conv.to_str(env.fetch(:scalar, '\\').get)
17
+ env.runtime.output(@handle, env).print(values.map { |cell| Conv.to_str(cell.get) }.join(separator), ending)
18
+ context == :list ? [Scalar.new(1)] : 1
19
+ end
20
+ end
21
+
22
+ # Formatted output using the Perl sprintf compatibility layer.
23
+ class Printf < Base
24
+ def initialize(handle, arguments)
25
+ @handle = handle
26
+ @arguments = arguments
27
+ end
28
+
29
+ def run(env, context)
30
+ values = @arguments&.run(env, :list) || []
31
+ format = Conv.to_str(values.shift&.get)
32
+ env.runtime.output(@handle, env).print(Sprintf.format(format, values.map(&:get)))
33
+ context == :list ? [Scalar.new(1)] : 1
34
+ end
35
+ end
36
+
37
+ # print with an implicit trailing newline.
38
+ class Say < Base
39
+ def initialize(handle, arguments)
40
+ @handle = handle
41
+ @arguments = arguments
42
+ end
43
+
44
+ def run(env, context)
45
+ values = @arguments ? @arguments.run(env, :list) : [env.fetch(:scalar, '_')]
46
+ env.runtime.output(@handle, env).puts(values.map { |cell| Conv.to_str(cell.get) }.join)
47
+ context == :list ? [Scalar.new(1)] : 1
48
+ end
49
+ end
50
+
51
+ # Diamond/readline operation in scalar or list context.
52
+ class Readline < Base
53
+ def initialize(handle)
54
+ @handle = handle
55
+ end
56
+
57
+ def run(env, context)
58
+ separator = env.fetch(:scalar, '/').get
59
+ if context == :list
60
+ values = []
61
+ loop do
62
+ record = read(env, separator)
63
+ break unless record
64
+
65
+ values << Scalar.new(record)
66
+ end
67
+ return values
68
+ end
69
+
70
+ read(env, separator)
71
+ end
72
+
73
+ private
74
+
75
+ def read(env, separator)
76
+ return env.runtime.read_argv(separator) if @handle.empty?
77
+
78
+ handle = resolve(env)
79
+ value = handle.read_record(separator)
80
+ env.runtime.note_input(handle) if value
81
+ value
82
+ end
83
+
84
+ def resolve(env)
85
+ return env.runtime.stash.glob('STDIN').io if @handle.nil? || @handle.empty?
86
+
87
+ if @handle.start_with?('$')
88
+ value = env.fetch(:scalar, @handle.delete_prefix('$')).get
89
+ return value if value.is_a?(IOHandle)
90
+ return value.target.io if value.is_a?(Ref) && value.target.is_a?(Glob)
91
+ end
92
+ lexical = env.fetch(:scalar, @handle).get
93
+ return lexical if lexical.is_a?(IOHandle)
94
+
95
+ env.runtime.stash.glob(@handle).io || raise(PerlError, "readline() on closed filehandle #{@handle}")
96
+ end
97
+ end
98
+
99
+ # Perl's readline-in-while shorthand assigns $_ and tests definedness.
100
+ class ReadlineCondition < Base
101
+ def initialize(readline)
102
+ @readline = readline
103
+ end
104
+
105
+ def run(env, context)
106
+ value = @readline.run(env, :scalar)
107
+ env.fetch(:scalar, '_').set(value)
108
+ result = value.nil? ? '' : 1
109
+ context == :list ? [Scalar.new(result)] : result
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ module Op
5
+ # Perl comma list with context-dependent evaluation.
6
+ class List < Base
7
+ def initialize(items)
8
+ @items = items
9
+ end
10
+
11
+ def run(env, context)
12
+ return @items.flat_map { |item| item.run(env, :list) } if context == :list
13
+ return nil if @items.empty?
14
+
15
+ @items[0...-1].each { |item| item.run(env, :void) }
16
+ @items.last.run(env, context)
17
+ end
18
+
19
+ def argument_cells(env, prototype = nil)
20
+ return prototype_cells(env, prototype) unless prototype.nil?
21
+ return @items.first.argument_cells(env) if @items.length == 1
22
+
23
+ @items.flat_map { |item| item.argument_cells(env) }
24
+ end
25
+
26
+ def argument_callable
27
+ return ->(_env, _prototype = nil) { [] } if @items.empty?
28
+
29
+ operations = @items.map(&:to_callable)
30
+ lambda do |env, prototype = nil|
31
+ next prototype_cells(env, prototype) if prototype
32
+
33
+ operations.flat_map { |operation| operation.call(env, :list) }
34
+ end
35
+ end
36
+
37
+ def empty? = @items.empty?
38
+
39
+ def lvalue(env, _context)
40
+ @items.flat_map do |item|
41
+ value = item.lvalue(env, :scalar)
42
+ value.is_a?(Array) ? value : [value]
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def prototype_cells(env, prototype)
49
+ specs = prototype.delete(';').scan(/\\?[$@%&*+_]/)
50
+ cells = []
51
+ @items.each_with_index do |item, index|
52
+ spec = specs[index]
53
+ if %w[@ %].include?(spec)
54
+ cells.concat(@items[index..].flat_map { |rest| rest.argument_cells(env) })
55
+ break
56
+ end
57
+
58
+ cells << prototype_cell(env, item, spec)
59
+ end
60
+ cells
61
+ end
62
+
63
+ def prototype_cell(env, item, spec)
64
+ return Scalar.new(Ref.new(item.aggregate(env, spec == '\\@' ? :array : :hash))) if %w[\@ \%].include?(spec)
65
+ return item.argument_cells(env).first if %w[& *].include?(spec)
66
+
67
+ cell = item.lvalue(env, :scalar)
68
+ cell.is_a?(Scalar) ? cell : Scalar.new(item.run(env, :scalar))
69
+ rescue PerlError
70
+ Scalar.new(item.run(env, :scalar))
71
+ end
72
+ end
73
+
74
+ # Anonymous array reference.
75
+ class ArrayLiteral < Base
76
+ def initialize(items)
77
+ @items = items
78
+ end
79
+
80
+ def run(env, context)
81
+ cells = @items.flat_map { |item| item.run(env, :list) }.map(&:copy)
82
+ value = Ref.new(PerlArray.new(cells))
83
+ context == :list ? [Scalar.new(value)] : value
84
+ end
85
+ end
86
+
87
+ # Anonymous hash reference.
88
+ class HashLiteral < Base
89
+ def initialize(items)
90
+ @items = items
91
+ end
92
+
93
+ def run(env, context)
94
+ values = @items.flat_map { |item| item.run(env, :list) }
95
+ hash = PerlHash.new
96
+ values.each_slice(2) { |key, value| hash.slot(Conv.to_str(key&.get)).set(value&.get) }
97
+ ref = Ref.new(hash)
98
+ context == :list ? [Scalar.new(ref)] : ref
99
+ end
100
+ end
101
+ end
102
+ end