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,378 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ module Op
5
+ # Runtime subroutine definition.
6
+ class SubDef < Base
7
+ def initialize(name, prototype, body)
8
+ @name = name
9
+ @prototype = prototype
10
+ @body = body
11
+ end
12
+
13
+ def run(env, _context)
14
+ install(env, @body)
15
+ end
16
+
17
+ def to_callable
18
+ body = Compiled.new(@body.to_callable, @body.to_subroutine_callable)
19
+ ->(env, _context) { install(env, body) }
20
+ end
21
+
22
+ private
23
+
24
+ def install(env, body)
25
+ package = @name.include?('::') ? @name.rpartition('::').first : env.package
26
+ env.runtime.warning('redefine', "Subroutine #{@name} redefined") if env.runtime.stash.fetch(@name)&.code
27
+ code = Code.new(@name, body, env.capture, package, @prototype)
28
+ env.runtime.stash.glob(@name).code = code
29
+ env.runtime.stash.touch
30
+ code
31
+ end
32
+ end
33
+
34
+ # Constant subroutine installed by use constant.
35
+ class ConstantDef < Base
36
+ def initialize(name, value)
37
+ @name = name
38
+ @value = value
39
+ end
40
+
41
+ def run(env, _context)
42
+ package = @name.include?('::') ? @name.rpartition('::').first : env.package
43
+ env.runtime.stash.glob(@name).code = Code.new(@name, @value, env.capture, package, '')
44
+ env.runtime.stash.touch
45
+ end
46
+ end
47
+
48
+ # Anonymous closure creation.
49
+ class AnonSub < Base
50
+ def initialize(prototype, body)
51
+ @prototype = prototype
52
+ @body = body
53
+ end
54
+
55
+ def run(env, context)
56
+ reference(env, context, @body)
57
+ end
58
+
59
+ def to_callable
60
+ body = Compiled.new(@body.to_callable, @body.to_subroutine_callable)
61
+ ->(env, context) { reference(env, context, body) }
62
+ end
63
+
64
+ private
65
+
66
+ def reference(env, context, body)
67
+ code = Code.new(nil, body, env.capture, env.package, @prototype)
68
+ value = Ref.new(code)
69
+ context == :list ? [Scalar.new(value)] : value
70
+ end
71
+ end
72
+
73
+ # User subroutine invocation.
74
+ class Call < Base
75
+ def initialize(name, arguments)
76
+ @name = name
77
+ @arguments = arguments
78
+ end
79
+
80
+ def run(env, context)
81
+ code = resolve(env)
82
+ env.runtime.call(code, @arguments.argument_cells(env, code.prototype), context)
83
+ end
84
+
85
+ def to_callable
86
+ arguments = @arguments.argument_callable
87
+ lambda do |env, context|
88
+ code = resolve(env)
89
+ env.runtime.call(code, arguments.call(env, code.prototype), context)
90
+ end
91
+ end
92
+
93
+ def scalar_callable
94
+ arguments = @arguments.argument_callable
95
+ lambda do |env|
96
+ code = resolve(env)
97
+ env.runtime.call(code, arguments.call(env, code.prototype), :scalar)
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def resolve(env)
104
+ stash = env.runtime.stash
105
+ unless @cached_generation == stash.generation
106
+ @cached_code = stash.glob(@name).code
107
+ @cached_generation = stash.generation
108
+ end
109
+ raise PerlError, "Undefined subroutine &#{@name} called" unless @cached_code
110
+
111
+ @cached_code
112
+ end
113
+ end
114
+
115
+ # Invocation through a code reference.
116
+ class CodeCall < Base
117
+ def initialize(receiver, arguments, inherit: false)
118
+ @receiver = receiver
119
+ @arguments = arguments
120
+ @inherit = inherit
121
+ end
122
+
123
+ def run(env, context)
124
+ code = @receiver.run(env, :scalar)
125
+ code = code.target if code.is_a?(Ref) && code.kind == 'CODE'
126
+ raise PerlError, 'Not a CODE reference' unless code.is_a?(Code)
127
+
128
+ arguments = @inherit ? env.fetch(:array, '_').cells.dup : @arguments.argument_cells(env)
129
+ env.runtime.call(code, arguments, context)
130
+ end
131
+ end
132
+
133
+ # Non-local subroutine return.
134
+ class Return < Base
135
+ def initialize(expression)
136
+ @expression = expression
137
+ end
138
+
139
+ def run(env, _context)
140
+ throw :peruby_return, @expression&.run(env, env.want)
141
+ end
142
+
143
+ def return_value(env, context)
144
+ @expression&.run(env, context)
145
+ end
146
+
147
+ def return_callable
148
+ expression = @expression&.to_callable
149
+ ->(env, context) { expression&.call(env, context) }
150
+ end
151
+ end
152
+
153
+ # Perl ref() result.
154
+ class RefKind < Base
155
+ def initialize(expression)
156
+ @expression = expression
157
+ end
158
+
159
+ def run(env, context)
160
+ value = @expression.run(env, :scalar)
161
+ result = value.is_a?(Ref) ? value.ref_string : ''
162
+ context == :list ? [Scalar.new(result)] : result
163
+ end
164
+ end
165
+
166
+ # Array push with autovivifying dereference support.
167
+ class Push < Base
168
+ def initialize(array, values)
169
+ @array = array
170
+ @values = values
171
+ end
172
+
173
+ def run(env, context)
174
+ array = @array.lvalue(env, :scalar)
175
+ raise PerlError, 'Type of arg 1 to push must be array' unless array.is_a?(PerlArray)
176
+
177
+ values = @values.flat_map { |value| value.argument_cells(env) }.map(&:copy)
178
+ array.cells.concat(values)
179
+ result = array.size
180
+ context == :list ? [Scalar.new(result)] : result
181
+ end
182
+ end
183
+
184
+ # Perl exists() over array/hash elements and slices.
185
+ class Exists < Base
186
+ def initialize(expression)
187
+ @expression = expression
188
+ end
189
+
190
+ def run(env, context)
191
+ result = @expression.respond_to?(:exists?) && @expression.exists?(env) ? 1 : ''
192
+ context == :list ? [Scalar.new(result)] : result
193
+ end
194
+ end
195
+
196
+ # Perl delete() over array/hash elements and slices.
197
+ class Delete < Base
198
+ def initialize(expression)
199
+ @expression = expression
200
+ end
201
+
202
+ def run(env, context)
203
+ values = @expression.respond_to?(:delete) ? @expression.delete(env) : []
204
+ return values.map { |value| Scalar.new(value) } if context == :list
205
+
206
+ values.last
207
+ end
208
+ end
209
+
210
+ # Blesses an existing reference into a package.
211
+ class Bless < Base
212
+ def initialize(arguments)
213
+ @arguments = arguments
214
+ end
215
+
216
+ def run(env, context)
217
+ reference = @arguments.first&.run(env, :scalar)
218
+ raise PerlError, 'Blessing non-reference value' unless reference.is_a?(Ref)
219
+
220
+ package = @arguments[1] ? Conv.to_str(@arguments[1].run(env, :scalar)) : env.package
221
+ reference.blessed = package
222
+ reference.runtime = env.runtime
223
+ env.runtime.register_object(reference)
224
+ context == :list ? [Scalar.new(reference)] : reference
225
+ end
226
+ end
227
+
228
+ # Object or class method invocation through Perl's package MRO.
229
+ class MethodCall < Base
230
+ def initialize(receiver, name, arguments)
231
+ @receiver = receiver
232
+ @name = name
233
+ @arguments = arguments
234
+ end
235
+
236
+ def run(env, context)
237
+ invoke(@receiver.run(env, :scalar), env, context)
238
+ end
239
+
240
+ def to_callable
241
+ receiver = @receiver.to_callable
242
+ scalar_receiver = @receiver.scalar_callable if @receiver.respond_to?(:scalar_callable)
243
+ arguments = @arguments.argument_callable
244
+ return static_callable(receiver, scalar_receiver, arguments, @arguments.empty?) unless @name.respond_to?(:run)
245
+
246
+ lambda do |env, context|
247
+ value = scalar_receiver ? scalar_receiver.call(env) : receiver.call(env, :scalar)
248
+ invoke(value, env, context, arguments.call(env))
249
+ end
250
+ end
251
+
252
+ private
253
+
254
+ # rubocop:disable-next Metrics/AbcSize
255
+ def static_callable(receiver, scalar_receiver, arguments, no_arguments)
256
+ method = @name
257
+ cached_package = cached_name = cached_super = cached_generation = cached_code = nil
258
+ lambda do |env, context|
259
+ value = scalar_receiver ? scalar_receiver.call(env) : receiver.call(env, :scalar)
260
+ next invoke(value, env, context, arguments.call(env)) if %w[isa DOES can VERSION].include?(method)
261
+
262
+ package = value.is_a?(Ref) ? value.blessed : Conv.to_str(value)
263
+ raise PerlError, "Can't call method #{method} on unblessed reference" if package.nil?
264
+
265
+ super_from = method.start_with?('SUPER::') ? env.package : nil
266
+ resolved_name = method.delete_prefix('SUPER::')
267
+ generation = env.runtime.stash.generation
268
+ unless cached_package == package && cached_name == resolved_name && cached_super == super_from &&
269
+ cached_generation == generation
270
+ cached_package = package
271
+ cached_name = resolved_name
272
+ cached_super = super_from
273
+ cached_generation = generation
274
+ cached_code = env.runtime.mro.resolve(package, resolved_name, super_from:)&.last
275
+ end
276
+ raise PerlError, "Can't locate object method #{resolved_name} via package #{package}" unless cached_code
277
+
278
+ cells = no_arguments ? [] : arguments.call(env)
279
+ env.runtime.call(cached_code, [Scalar.new(value), *cells], context)
280
+ end
281
+ end
282
+
283
+ def invoke(receiver, env, context, argument_cells = @arguments.argument_cells(env))
284
+ method = method_name(env)
285
+ if method.is_a?(Code)
286
+ arguments = [Scalar.new(receiver), *argument_cells]
287
+ return env.runtime.call(method, arguments, context)
288
+ end
289
+ return universal(receiver, method, env, context) if %w[isa DOES can VERSION].include?(method)
290
+
291
+ package = receiver.is_a?(Ref) ? receiver.blessed : Conv.to_str(receiver)
292
+ raise PerlError, "Can't call method #{method} on unblessed reference" if package.nil?
293
+
294
+ super_from = method.start_with?('SUPER::') ? env.package : nil
295
+ method = method.delete_prefix('SUPER::')
296
+ code = resolved_method(env, package, method, super_from)
297
+ raise PerlError, "Can't locate object method #{method} via package #{package}" unless code
298
+
299
+ arguments = [Scalar.new(receiver), *argument_cells]
300
+ env.runtime.call(code, arguments, context)
301
+ end
302
+
303
+ def resolved_method(env, package, method, super_from)
304
+ stash = env.runtime.stash
305
+ key = [package, method, super_from]
306
+ unless @cached_method_key == key && @cached_method_generation == stash.generation
307
+ @cached_method_key = key
308
+ @cached_method_generation = stash.generation
309
+ @cached_method = env.runtime.mro.resolve(package, method, super_from:)&.last
310
+ end
311
+ @cached_method
312
+ end
313
+
314
+ def method_name(env)
315
+ return @name unless @name.respond_to?(:run)
316
+
317
+ value = @name.run(env, :scalar)
318
+ return value.target if value.is_a?(Ref) && value.kind == 'CODE'
319
+
320
+ Conv.to_str(value)
321
+ end
322
+
323
+ def universal(receiver, method, env, context)
324
+ package = receiver.is_a?(Ref) ? receiver.blessed : Conv.to_str(receiver)
325
+ argument = @arguments.run(env, :scalar)
326
+ value = case method
327
+ when 'isa', 'DOES' then env.runtime.mro.isa?(package, Conv.to_str(argument)) ? 1 : ''
328
+ when 'can' then can(package, Conv.to_str(argument), env)
329
+ when 'VERSION' then version(package, argument, env)
330
+ end
331
+ context == :list ? [Scalar.new(value)] : value
332
+ end
333
+
334
+ def can(package, method, env)
335
+ entry = env.runtime.mro.resolve(package, method, autoload: false)
336
+ entry && Ref.new(entry.last)
337
+ end
338
+
339
+ def version(package, required, env)
340
+ version = env.fetch(:scalar, "#{package}::VERSION").get
341
+ if !required.nil? && Conv.to_num(version) < Conv.to_num(required)
342
+ message = "#{package} version #{Conv.to_str(required)} required--this is only version #{Conv.to_str(version)}"
343
+ raise PerlError, message
344
+ end
345
+
346
+ version
347
+ end
348
+ end
349
+
350
+ # Tail-call style goto &sub preserving the current @_ cells.
351
+ class Goto < Base
352
+ def initialize(name)
353
+ @name = name
354
+ end
355
+
356
+ def run(env, _context)
357
+ code = env.runtime.stash.glob(@name).code || env.runtime.stash.glob("#{env.package}::#{@name}").code
358
+ raise PerlError, "Undefined subroutine &#{@name} called" unless code
359
+
360
+ throw :peruby_return, GotoRequest.new(code)
361
+ end
362
+ end
363
+
364
+ # undef with optional deterministic blessed-reference release.
365
+ class Undef < Base
366
+ def initialize(expression)
367
+ @expression = expression
368
+ end
369
+
370
+ def run(env, _context)
371
+ cell = @expression.lvalue(env, :scalar)
372
+ env.runtime.release(cell.get)
373
+ cell.set(nil)
374
+ nil
375
+ end
376
+ end
377
+ end
378
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ module Op
5
+ # Shared label-aware last/next/redo handling.
6
+ module LoopExecution
7
+ private
8
+
9
+ def run_iteration?(env, body = @body)
10
+ loop do
11
+ body.run(env, :void)
12
+ break
13
+ rescue LoopControl => e
14
+ raise unless e.label.nil? || e.label == @label
15
+
16
+ return false if e.kind == :last
17
+ break if e.kind == :next
18
+ end
19
+ @continue&.run(env, :void)
20
+ true
21
+ end
22
+ end
23
+
24
+ # Ordered statement execution, optionally in a lexical scope.
25
+ class Sequence < Base
26
+ def initialize(operations, scoped: false)
27
+ @operations = operations
28
+ @scoped = scoped
29
+ @warning_scope = operations.any?(&:warning_directive?)
30
+ end
31
+
32
+ def run(env, context)
33
+ execute = -> { execute_operations(env, context) }
34
+ scoped = -> { @scoped ? env.with_scope(&execute) : execute.call }
35
+ @warning_scope ? env.runtime.with_warning_scope(&scoped) : scoped.call
36
+ end
37
+
38
+ def run_subroutine(env, context)
39
+ if !@scoped && @operations.last.respond_to?(:return_value)
40
+ @operations[0...-1].each { |operation| operation.run(env, :void) }
41
+ return @operations.last.return_value(env, context)
42
+ end
43
+
44
+ catch(:peruby_return) { run(env, context) }
45
+ end
46
+
47
+ def to_callable
48
+ operations = @operations.map(&:to_callable)
49
+ execute = lambda do |env, context|
50
+ next nil if operations.empty?
51
+
52
+ operations[0...-1].each { |operation| operation.call(env, :void) }
53
+ operations.last.call(env, context)
54
+ end
55
+ callable = @scoped ? ->(env, context) { env.with_scope { execute.call(env, context) } } : execute
56
+ warning_callable(callable)
57
+ end
58
+
59
+ def to_subroutine_callable
60
+ operations = @operations.map(&:to_callable)
61
+ if !@scoped && @operations.last.respond_to?(:return_callable)
62
+ returning = @operations.last.return_callable
63
+ callable = lambda do |env, context|
64
+ operations[0...-1].each { |operation| operation.call(env, :void) }
65
+ returning.call(env, context)
66
+ end
67
+ return warning_callable(callable)
68
+ end
69
+
70
+ callable = to_callable
71
+ ->(env, context) { catch(:peruby_return) { callable.call(env, context) } }
72
+ end
73
+
74
+ def to_void_callable
75
+ operations = @operations.map do |operation|
76
+ next operation.to_void_callable if operation.respond_to?(:to_void_callable)
77
+
78
+ callable = operation.to_callable
79
+ ->(env) { callable.call(env, :void) }
80
+ end
81
+ execute = ->(env) { operations.each { |operation| operation.call(env) } }
82
+ callable = @scoped ? ->(env) { env.with_scope { execute.call(env) } } : execute
83
+ return callable unless @warning_scope
84
+
85
+ ->(env) { env.runtime.with_warning_scope { callable.call(env) } }
86
+ end
87
+
88
+ private
89
+
90
+ def warning_callable(callable)
91
+ return callable unless @warning_scope
92
+
93
+ ->(env, context) { env.runtime.with_warning_scope { callable.call(env, context) } }
94
+ end
95
+
96
+ def execute_operations(env, context)
97
+ return nil if @operations.empty?
98
+
99
+ @operations[0...-1].each { |operation| operation.run(env, :void) }
100
+ @operations.last.run(env, context)
101
+ end
102
+ end
103
+
104
+ # Conditional branch operation.
105
+ class Conditional < Base
106
+ def initialize(condition, then_branch, else_branch)
107
+ @condition = condition
108
+ @then_branch = then_branch
109
+ @else_branch = else_branch
110
+ end
111
+
112
+ def run(env, context)
113
+ branch = Conv.truthy?(@condition.run(env, :scalar)) ? @then_branch : @else_branch
114
+ branch&.run(env, context)
115
+ end
116
+
117
+ def to_callable
118
+ condition = @condition.to_callable
119
+ scalar_condition = @condition.scalar_callable if @condition.respond_to?(:scalar_callable)
120
+ then_branch = @then_branch.to_callable
121
+ else_branch = @else_branch&.to_callable
122
+ lambda do |env, context|
123
+ value = scalar_condition ? scalar_condition.call(env) : condition.call(env, :scalar)
124
+ branch = Conv.truthy?(value) ? then_branch : else_branch
125
+ branch&.call(env, context)
126
+ end
127
+ end
128
+ end
129
+
130
+ # Changes the package used for unqualified global names.
131
+ class PackageSwitch < Base
132
+ def initialize(name) = @name = name
133
+ def run(env, _context) = env.package = @name
134
+ end
135
+
136
+ # Temporarily changes the current package for one lexical block.
137
+ class PackageBlock < Base
138
+ def initialize(name, body)
139
+ @name = name
140
+ @body = body
141
+ end
142
+
143
+ def run(env, context) = env.with_package(@name) { @body.run(env, context) }
144
+
145
+ def to_callable
146
+ body = @body.to_callable
147
+ ->(env, context) { env.with_package(@name) { body.call(env, context) } }
148
+ end
149
+ end
150
+
151
+ # A standalone Perl block behaves as a one-iteration loop.
152
+ class BareBlock < Base
153
+ def initialize(body, label)
154
+ @body = body
155
+ @label = label
156
+ end
157
+
158
+ def run(env, context)
159
+ loop do
160
+ return @body.run(env, context)
161
+ rescue LoopControl => e
162
+ raise unless e.label.nil? || e.label == @label
163
+
164
+ return nil unless e.kind == :redo
165
+ end
166
+ end
167
+ end
168
+
169
+ # While or until loop operation.
170
+ class While < Base
171
+ include LoopExecution
172
+
173
+ def initialize(condition, body, until_loop, continuation = nil, label = nil)
174
+ @condition = condition
175
+ @body = body
176
+ @until_loop = until_loop
177
+ @continue = continuation
178
+ @label = label
179
+ end
180
+
181
+ def run(env, _context)
182
+ loop do
183
+ truth = Conv.truthy?(@condition.run(env, :scalar))
184
+ break if @until_loop ? truth : !truth
185
+
186
+ break unless run_iteration?(env)
187
+ end
188
+ nil
189
+ end
190
+
191
+ def to_callable
192
+ condition = @condition.to_callable
193
+ scalar_condition = @condition.scalar_callable if @condition.respond_to?(:scalar_callable)
194
+ body = @body.to_callable
195
+ void_body = @body.to_void_callable if @body.respond_to?(:to_void_callable)
196
+ continuation = @continue&.to_callable
197
+ lambda do |env, _context|
198
+ loop do
199
+ value = scalar_condition ? scalar_condition.call(env) : condition.call(env, :scalar)
200
+ truth = Conv.truthy?(value)
201
+ break if @until_loop ? truth : !truth
202
+
203
+ keep_going = compiled_iteration?(env, body, void_body, continuation)
204
+ break unless keep_going
205
+ end
206
+ nil
207
+ end
208
+ end
209
+
210
+ private
211
+
212
+ def compiled_iteration?(env, body, void_body, continuation)
213
+ loop do
214
+ void_body ? void_body.call(env) : body.call(env, :void)
215
+ break
216
+ rescue LoopControl => e
217
+ raise unless e.label.nil? || e.label == @label
218
+
219
+ return false if e.kind == :last
220
+ break if e.kind == :next
221
+ end
222
+ continuation&.call(env, :void)
223
+ true
224
+ end
225
+ end
226
+
227
+ # Block and string eval sharing the current lexical environment.
228
+ class Eval < Base
229
+ def initialize(body, string, file: '-e')
230
+ @body = body
231
+ @string = string
232
+ @file = file
233
+ end
234
+
235
+ def run(env, context)
236
+ env.fetch(:scalar, '@').set('')
237
+ result = if @string
238
+ env.with_scope do
239
+ source = Conv.to_str(@body.run(env, :scalar))
240
+ operation = CompileUnit.new(runtime: env.runtime, file: @file).compile(source, env:)
241
+ operation.run_subroutine(env, context)
242
+ end
243
+ else
244
+ @body.run_subroutine(env, context)
245
+ end
246
+ env.fetch(:scalar, '@').set('')
247
+ result
248
+ rescue Error => e
249
+ value = e.is_a?(PerlError) ? e.value : e.message
250
+ value = env.runtime.error_message(value) unless value.is_a?(Ref)
251
+ env.fetch(:scalar, '@').set(value)
252
+ context == :list ? [] : nil
253
+ end
254
+ end
255
+ end
256
+ end