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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Minimal TAP producer shared by Test::More and Test::Simple.
5
+ class TestBuilder
6
+ Context = Struct.new(:tests_run, :planned, :indent)
7
+
8
+ def initialize(output)
9
+ @output = output
10
+ @contexts = [Context.new(0, nil, '')]
11
+ end
12
+
13
+ def plan(count)
14
+ current.planned = count.to_i
15
+ @output.puts "#{current.indent}1..#{current.planned}"
16
+ 1
17
+ end
18
+
19
+ def ok(success, name = '')
20
+ current.tests_run += 1
21
+ label = name.to_s.empty? ? '' : " - #{name}"
22
+ @output.puts "#{current.indent}#{success ? 'ok' : 'not ok'} #{current.tests_run}#{label}"
23
+ success ? 1 : ''
24
+ end
25
+
26
+ def done_testing
27
+ @output.puts "#{current.indent}1..#{current.tests_run}" unless current.planned
28
+ 1
29
+ end
30
+
31
+ def subtest(name)
32
+ @output.puts "#{current.indent}# Subtest: #{name}"
33
+ @contexts << Context.new(0, nil, "#{current.indent} ")
34
+ yield
35
+ done_testing
36
+ @contexts.pop
37
+ ok(true, name)
38
+ rescue StandardError
39
+ @contexts.pop if @contexts.length > 1
40
+ ok(false, name)
41
+ end
42
+
43
+ private
44
+
45
+ def current = @contexts.last
46
+ end
47
+ end
@@ -0,0 +1,325 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runtime/env'
4
+ require_relative 'runtime/interpolation'
5
+ require_relative 'runtime/code'
6
+
7
+ module Peruby
8
+ # Process-wide interpreter state.
9
+ class Runtime # rubocop:disable Metrics/ClassLength
10
+ attr_reader :stash, :stdout, :stderr, :match_state, :mro, :module_loader, :test_builder, :current_file,
11
+ :local_stack,
12
+ :file_stat_path, :last_input_handle
13
+
14
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
15
+ def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr, refcount: false)
16
+ @stash = Stash.new
17
+ @stdout = stdout
18
+ @stderr = stderr
19
+ @match_state = MatchState.new
20
+ @local_stack = LocalStack.new
21
+ @mro = MRO.new(@stash)
22
+ @refcount = refcount
23
+ @objects = {}.compare_by_identity
24
+ @destroyed = {}.compare_by_identity
25
+ @module_loader = ModuleLoader.new(self)
26
+ @test_builder = TestBuilder.new(stdout)
27
+ @current_file = '-e'
28
+ @phases = Hash.new { |phases, name| phases[name] = [] }
29
+ @overloads = {}
30
+ @warnings = Set.new
31
+ @warning_switch = @stash.glob('^W').scalar
32
+ @stash.glob('$').scalar.set(Process.pid)
33
+ @stash.glob('SIG').hash = PerlHash.new { |name, value| install_signal(name, value) }
34
+ env_cells = ENV.to_h { |name, value| [name, Scalar.new(value)] }
35
+ @stash.glob('ENV').hash = PerlHash.new(env_cells) { |name, value| update_environment(name, value) }
36
+ @stash.glob('"').scalar.set(' ')
37
+ @stash.glob(',').scalar.set(nil)
38
+ @stash.glob('\\').scalar.set(nil)
39
+ @stash.glob('/').scalar.set("\n")
40
+ @stdin_handle = IOHandle.new(stdin)
41
+ @stash.glob('STDIN').io = @stdin_handle
42
+ @stash.glob('STDOUT').io = IOHandle.new(stdout)
43
+ @stash.glob('STDERR').io = IOHandle.new(stderr)
44
+ @stash.glob('ARGVOUT').io = IOHandle.new(stdout)
45
+ @selected_output = @stash.glob('STDOUT').io
46
+ @stash.glob('|').scalar = Scalar.new { |value| @selected_output.io.sync = Conv.truthy?(value) }
47
+ @stash.glob('INC').array.cells << Scalar.new('.')
48
+ end
49
+
50
+ def env
51
+ Env.new(self)
52
+ end
53
+
54
+ def output(handle, env = nil)
55
+ return @selected_output.io unless handle
56
+
57
+ if env && handle
58
+ lexical = env.fetch(:scalar, handle).get
59
+ return lexical.io if lexical.is_a?(IOHandle)
60
+ end
61
+ @stash.glob(handle).io&.io || (handle == 'STDERR' ? @stderr : @stdout)
62
+ end
63
+
64
+ def select_output(handle = nil)
65
+ previous = @selected_output
66
+ @selected_output = handle if handle
67
+ previous
68
+ end
69
+
70
+ def read_argv(separator)
71
+ loop do
72
+ open_next_argv unless @argv_handle || @argv_finished
73
+ return nil unless @argv_handle
74
+
75
+ value = @argv_handle.read_record(separator)
76
+ if value
77
+ @last_input_handle = @argv_handle
78
+ @argv_lineno = (@argv_lineno || 0) + 1
79
+ @stash.glob('.').scalar.set(@argv_lineno)
80
+ return value
81
+ end
82
+ @argv_handle.close unless @argv_handle.equal?(@stdin_handle)
83
+ @argv_handle = nil
84
+ end
85
+ end
86
+
87
+ def note_input(handle)
88
+ @last_input_handle = handle
89
+ @stash.glob('.').scalar.set(handle.lineno)
90
+ end
91
+
92
+ def match!(match, subject)
93
+ @match_state = MatchState.new(match, subject)
94
+ end
95
+
96
+ def with_match_scope
97
+ previous = @match_state
98
+ yield
99
+ ensure
100
+ @match_state = previous
101
+ end
102
+
103
+ def with_file(file)
104
+ previous = @current_file
105
+ @current_file = file
106
+ yield
107
+ ensure
108
+ @current_file = previous
109
+ end
110
+
111
+ def error_message(message, file: @current_file, line: 1)
112
+ message.end_with?("\n") ? message : "#{message} at #{file} line #{line}.\n"
113
+ end
114
+
115
+ def die(values, file: @current_file, line: 1)
116
+ object = values.one? && values.first.is_a?(Ref) ? values.first : nil
117
+ text = values.map { |value| Conv.to_str(value) }.join
118
+ message = object ? Conv.to_str(object) : error_message(text, file:, line:)
119
+ handler = @stash.glob('SIG').hash.fetch('__DIE__')
120
+ handler = handler.target if handler.is_a?(Ref) && handler.kind == 'CODE'
121
+ previous = @handling_die
122
+ @handling_die = true
123
+ call(handler, [Scalar.new(object || message)], :void) if handler.is_a?(Code) && !previous
124
+ raise PerlError.new(message, value: object || message, formatted: true)
125
+ ensure
126
+ @handling_die = previous
127
+ end
128
+
129
+ def call(code, arguments, context)
130
+ return call_compiled(code, arguments, context) if code.body.is_a?(Op::Compiled)
131
+
132
+ validate_prototype(code, arguments.length)
133
+ with_match_scope do
134
+ @local_stack.within do
135
+ loop do
136
+ env = code.environment.fork(want: context, package: code.package, state_owner: code)
137
+ env.bind(:array, '_', PerlArray.new(arguments))
138
+ result = code.body.run_subroutine(env, context)
139
+ if result.is_a?(GotoRequest)
140
+ code = result.code
141
+ next
142
+ end
143
+ return context == :list ? Array(result).map(&:copy) : result
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ def call_compiled(code, arguments, context)
150
+ validate_prototype(code, arguments.length) if code.prototype
151
+ previous_match = @match_state
152
+ @local_stack.within do
153
+ loop do
154
+ env = code.environment.call_frame(arguments, want: context, package: code.package, state_owner: code)
155
+ result = code.body.run_subroutine(env, context)
156
+ if result.is_a?(GotoRequest)
157
+ code = result.code
158
+ next
159
+ end
160
+ return context == :list ? Array(result).map(&:copy) : result
161
+ end
162
+ end
163
+ ensure
164
+ @match_state = previous_match
165
+ end
166
+
167
+ def validate_prototype(code, count)
168
+ prototype = code.prototype
169
+ return if prototype.nil?
170
+
171
+ required, _, optional = prototype.partition(';')
172
+ minimum = prototype_arity(required.delete_suffix('@').delete_suffix('%'))
173
+ maximum = required.match?(/[@%]\z/) ? nil : minimum + prototype_arity(optional)
174
+ return if count >= minimum && (maximum.nil? || count <= maximum)
175
+
176
+ raise PerlError, "Wrong number of arguments for #{code.name || 'anonymous subroutine'}"
177
+ end
178
+
179
+ def prototype_arity(prototype)
180
+ prototype.scan(/\\?[$@%&*+_]/).length
181
+ end
182
+
183
+ def with_warning_scope
184
+ previous = @warnings.dup
185
+ yield
186
+ ensure
187
+ @warnings = previous
188
+ end
189
+
190
+ def configure_warnings(categories, disable: false)
191
+ selected = categories.empty? ? %w[uninitialized numeric once redefine] : categories
192
+ disable ? @warnings.subtract(selected) : @warnings.merge(selected)
193
+ end
194
+
195
+ def warning_enabled?(category)
196
+ @warnings.include?(category) || Conv.truthy?(@warning_switch.get)
197
+ end
198
+
199
+ def warnings_active?
200
+ !@warnings.empty? || Conv.truthy?(@warning_switch.get)
201
+ end
202
+
203
+ def warning_categories = @warnings.dup
204
+
205
+ def warning(category, message)
206
+ return unless warning_enabled?(category)
207
+
208
+ emit_warning(message)
209
+ end
210
+
211
+ def emit_warning(message, file: @current_file, line: 1)
212
+ formatted = error_message(message, file:, line:)
213
+ handler = @stash.glob('SIG').hash.fetch('__WARN__')
214
+ handler = handler.target if handler.is_a?(Ref) && handler.kind == 'CODE'
215
+ return call(handler, [Scalar.new(formatted)], :void) if handler.is_a?(Code)
216
+
217
+ @stderr.print(formatted)
218
+ end
219
+
220
+ def number(value)
221
+ warning('uninitialized', 'Use of uninitialized value') if value.nil?
222
+ warning('numeric', %(Argument "#{value}" isn't numeric)) if value.is_a?(String) && !Conv::NUM_RE.match?(value)
223
+ Conv.to_num(value)
224
+ end
225
+
226
+ def file_stat(path, lstat: false)
227
+ return @file_stat if path == '_'
228
+
229
+ @file_stat_path = path
230
+ @file_stat = nil
231
+ @file_stat = File.public_send(lstat ? :lstat : :stat, path)
232
+ end
233
+
234
+ def register_object(reference)
235
+ @objects[reference] = true
236
+ end
237
+
238
+ def release(reference)
239
+ destroy(reference) if @refcount
240
+ end
241
+
242
+ def shutdown
243
+ run_phase(:end, reverse: true)
244
+ @objects.each_key { |reference| destroy(reference) }
245
+ end
246
+
247
+ def install_signal(name, value)
248
+ return if %w[__DIE__ __WARN__].include?(name)
249
+
250
+ handler = value.is_a?(Ref) && value.kind == 'CODE' ? value.target : value
251
+ return Signal.trap(name, handler || 'DEFAULT') unless handler.is_a?(Code)
252
+
253
+ Signal.trap(name) { call(handler, [Scalar.new(name)], :void) }
254
+ rescue ArgumentError => e
255
+ raise PerlError, e.message
256
+ end
257
+
258
+ def update_environment(name, value)
259
+ value.nil? ? ENV.delete(name) : ENV[name] = Conv.to_str(value)
260
+ end
261
+
262
+ def register_overload(package, pairs, disable: false)
263
+ return @overloads.delete(package) if disable
264
+
265
+ @overloads[package] = pairs
266
+ end
267
+
268
+ def overloads? = !@overloads.empty?
269
+
270
+ def overloaded(reference, operator, other = nil, swapped: false)
271
+ table = overload_table(reference)
272
+ return [false, nil] unless table
273
+
274
+ method = table[operator] || table['nomethod']
275
+ return [false, nil] unless method
276
+
277
+ entry = @mro.resolve(reference.blessed, Conv.to_str(method), autoload: false)
278
+ return [false, nil] unless entry
279
+
280
+ arguments = [Scalar.new(reference), Scalar.new(other), Scalar.new(swapped ? 1 : '')]
281
+ arguments << Scalar.new(operator) if table[operator].nil?
282
+ [true, call(entry.last, arguments, :scalar)]
283
+ end
284
+
285
+ def overload_table(reference)
286
+ return unless reference.is_a?(Ref) && reference.blessed
287
+
288
+ @mro.lineage(reference.blessed).filter_map { |package| @overloads[package] }.first
289
+ end
290
+
291
+ def open_next_argv
292
+ cell = @stash.glob('ARGV').array.cells.shift
293
+ if cell
294
+ path = Conv.to_str(cell.get)
295
+ @stash.glob('ARGV').scalar.set(path)
296
+ @argv_handle = path == '-' ? @stdin_handle : IOHandle.new(File.open(path))
297
+ elsif !defined?(@argv_started)
298
+ @argv_started = true
299
+ @stash.glob('ARGV').scalar.set('-')
300
+ @argv_handle = @stdin_handle
301
+ else
302
+ @argv_finished = true
303
+ end
304
+ end
305
+
306
+ def register_phase(kind, operation, env: self.env)
307
+ @phases[kind] << [operation, env]
308
+ end
309
+
310
+ def run_phase(kind, reverse: false)
311
+ operations = @phases.delete(kind) || []
312
+ operations.reverse! if reverse
313
+ operations.each { |operation, phase_env| operation.run(phase_env, :void) }
314
+ end
315
+
316
+ def destroy(reference)
317
+ return unless reference.is_a?(Ref) && reference.blessed
318
+ return if @destroyed[reference]
319
+
320
+ @destroyed[reference] = true
321
+ entry = @mro.resolve(reference.blessed, 'DESTROY')
322
+ call(entry.last, [Scalar.new(reference)], :void) if entry
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Compile-time lexical checks for the strict pragma.
5
+ class Validator
6
+ State = Data.define(:strict, :warnings, :declared) do
7
+ def nested = State.new(strict.dup, warnings.dup, declared.dup)
8
+ end
9
+
10
+ SPECIAL = %w[_ a b ARGV ENV INC SIG].freeze
11
+
12
+ def self.validate(node, runtime: nil, strict: Set.new, warnings: Set.new)
13
+ new(runtime, strict, warnings).validate(node)
14
+ end
15
+
16
+ def initialize(runtime = nil, strict = Set.new, warnings = Set.new)
17
+ @runtime = runtime
18
+ @strict = strict
19
+ @warnings = warnings
20
+ @package_uses = Hash.new { |uses, key| uses[key] = [] }
21
+ @loop_labels = []
22
+ end
23
+
24
+ def validate(node)
25
+ visit(node, State.new(@strict.dup, @warnings.dup, {}))
26
+ warn_once
27
+ node
28
+ end
29
+
30
+ private
31
+
32
+ def visit(node, state)
33
+ return if node.nil?
34
+ return node.each { |item| visit(item, state) } if node.is_a?(Array)
35
+ return unless node.class.name.start_with?('Peruby::Node::')
36
+
37
+ name = node.class.name.split('::').last.gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2')
38
+ .gsub(/([a-z\d])([A-Z])/, '\\1_\\2').downcase
39
+ method = "visit_#{name}"
40
+ respond_to?(method, true) ? send(method, node, state) : visit_members(node, state)
41
+ end
42
+
43
+ def visit_program(node, state) = visit_sequence(node.statements, state)
44
+ def visit_block(node, state) = visit_sequence(node.statements, state.nested)
45
+
46
+ def visit_sequence(statements, state)
47
+ statements.each do |statement|
48
+ if statement.is_a?(Node::Use) && statement.module_name == 'strict'
49
+ update_strict(statement, state)
50
+ elsif statement.is_a?(Node::Use) && statement.module_name == 'warnings'
51
+ update_warnings(statement, state)
52
+ elsif statement.is_a?(Node::Use) && statement.module_name == 'vars'
53
+ update_vars(statement, state)
54
+ else
55
+ visit(statement, state)
56
+ end
57
+ end
58
+ end
59
+
60
+ def update_vars(node, state)
61
+ return if node.disable
62
+
63
+ sigils = { '$' => :scalar, '@' => :array, '%' => :hash }
64
+ node.imports.each do |name|
65
+ text = name.to_s
66
+ sigil = sigils[text[0]]
67
+ state.declared[[sigil, text[1..]]] = :package if sigil && text.length > 1
68
+ end
69
+ end
70
+
71
+ def visit_my(node, state) = visit_declaration(node, state, :lexical)
72
+ def visit_state(node, state) = visit_declaration(node, state, :lexical)
73
+ def visit_our(node, state) = visit_declaration(node, state, :package)
74
+
75
+ def visit_declaration(node, state, kind)
76
+ visit(node.initializer, state)
77
+ variables = node.variable.is_a?(Node::List) ? node.variable.items : [node.variable]
78
+ variables.each do |variable|
79
+ name = kind == :package ? variable.name.rpartition('::').last : variable.name
80
+ state.declared[[variable.sigil, name]] = kind
81
+ record_package_use(variable, state) if kind == :package
82
+ end
83
+ end
84
+
85
+ def visit_variable(node, state)
86
+ declaration = state.declared[[node.sigil, node.name]]
87
+ record_package_use(node, state) if declaration == :package || (declaration.nil? && package_variable?(node))
88
+ return unless state.strict.include?('vars')
89
+ return if node.name.include?('::') || SPECIAL.include?(node.name) || special_variable?(node)
90
+ return if declaration
91
+
92
+ error(node, "Global symbol \"#{sigil(node.sigil)}#{node.name}\" requires explicit package name")
93
+ end
94
+
95
+ def visit_bareword(node, state)
96
+ error(node, "Bareword \"#{node.value}\" not allowed while strict subs in use") if state.strict.include?('subs')
97
+ end
98
+
99
+ def visit_literal(node, state)
100
+ value = node.value
101
+ source = value.body if value.is_a?(Lexer::Heredoc) && value.interpolate
102
+ source = value.parts.first if value.is_a?(Lexer::Quote) && value.interpolate
103
+ return unless source
104
+
105
+ name = Interpolation::NAME.source
106
+ source.scan(/(?<!\\)([$@])(?:\{(#{name})\}|(#{name}))/) do |sigil, braced, plain|
107
+ kind = sigil == '$' ? :scalar : :array
108
+ visit_variable(Node::Variable.new(kind, braced || plain, node.file, node.line), state)
109
+ end
110
+ source.scan(/(?<!\\)\$#(#{name})/) do |match|
111
+ visit_variable(Node::Variable.new(:array, match.first, node.file, node.line), state)
112
+ end
113
+ end
114
+
115
+ def visit_while(node, state)
116
+ visit(node.condition, state)
117
+ visit_loop(node.label) do
118
+ visit(node.body, state)
119
+ visit(node.continue_block, state)
120
+ end
121
+ end
122
+
123
+ def visit_bare_block(node, state)
124
+ visit_loop(node.label) { visit(node.body, state) }
125
+ end
126
+
127
+ def visit_c_for(node, state)
128
+ loop_state = state.nested
129
+ visit(node.initializer, loop_state)
130
+ visit(node.condition, loop_state)
131
+ visit_loop(node.label) do
132
+ visit(node.body, loop_state)
133
+ visit(node.continue_block, loop_state)
134
+ visit(node.step, loop_state)
135
+ end
136
+ end
137
+
138
+ def visit_for_each(node, state)
139
+ visit(node.list, state)
140
+ loop_state = state.nested
141
+ if node.declaration
142
+ loop_state.declared[[node.variable.sigil, node.variable.name]] = :lexical
143
+ else
144
+ visit(node.variable, state)
145
+ end
146
+ visit_loop(node.label) do
147
+ visit(node.body, loop_state)
148
+ visit(node.continue_block, loop_state)
149
+ end
150
+ end
151
+
152
+ def visit_modifier_condition(node, state)
153
+ visit(node.condition, state)
154
+ if %i[while until].include?(node.kind)
155
+ visit_loop(nil) { visit(node.expression, state) }
156
+ else
157
+ visit(node.expression, state)
158
+ end
159
+ end
160
+
161
+ def visit_modifier_for(node, state)
162
+ visit(node.list, state)
163
+ visit_loop(nil) { visit(node.expression, state) }
164
+ end
165
+
166
+ def visit_loop_jump(node, _state)
167
+ valid = node.label ? @loop_labels.include?(node.label) : !@loop_labels.empty?
168
+ return if valid
169
+
170
+ message = if node.label
171
+ %(Label not found for "#{node.kind} #{node.label}")
172
+ else
173
+ %(Can't "#{node.kind}" outside a loop block)
174
+ end
175
+ error(node, message)
176
+ end
177
+
178
+ def visit_element(node, state)
179
+ container = node.container
180
+ if container.is_a?(Node::Variable) && container.sigil != node.kind
181
+ container = Node::Variable.new(node.kind, container.name, container.file, container.line)
182
+ end
183
+ visit(container, state)
184
+ visit(node.key, state) unless node.kind == :hash && node.key.is_a?(Node::Bareword)
185
+ end
186
+
187
+ def visit_members(node, state)
188
+ node.members.reject { |member| %i[file line].include?(member) }.each do |member|
189
+ visit(node.public_send(member), state)
190
+ end
191
+ end
192
+
193
+ def visit_loop(label)
194
+ @loop_labels << label
195
+ yield
196
+ ensure
197
+ @loop_labels.pop
198
+ end
199
+
200
+ def update_strict(node, state)
201
+ categories = node.imports.empty? ? %w[refs subs vars] : node.imports
202
+ node.disable ? state.strict.subtract(categories) : state.strict.merge(categories)
203
+ end
204
+
205
+ def update_warnings(node, state)
206
+ categories = node.imports.empty? ? %w[uninitialized numeric once redefine] : node.imports
207
+ node.disable ? state.warnings.subtract(categories) : state.warnings.merge(categories)
208
+ end
209
+
210
+ def package_variable?(node)
211
+ !SPECIAL.include?(node.name) && !special_variable?(node)
212
+ end
213
+
214
+ def special_variable?(node) = node.name.match?(/\A(?:\d+|\W|\^.)\z/)
215
+
216
+ def record_package_use(node, state)
217
+ @package_uses[[node.sigil, node.name]] << [node, state.warnings.include?('once')]
218
+ end
219
+
220
+ def warn_once
221
+ @package_uses.each do |(_sigil, name), uses|
222
+ next unless uses.one? && uses.first.last && @runtime
223
+
224
+ node = uses.first.first
225
+ symbol = name.include?('::') ? name : "main::#{name}"
226
+ @runtime.emit_warning(%(Name "#{symbol}" used only once: possible typo), file: node.file, line: node.line)
227
+ end
228
+ end
229
+
230
+ def sigil(kind) = { scalar: '$', array: '@', hash: '%', code: '&', glob: '*' }.fetch(kind, '$')
231
+
232
+ def error(node, message)
233
+ raise CompileError, "#{message} at #{node.file} line #{node.line}."
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ VERSION = '0.1.0'
5
+ end
data/lib/peruby.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'peruby/version'
4
+ require_relative 'peruby/errors'
5
+ require_relative 'peruby/runtime/scalar'
6
+ require_relative 'peruby/runtime/perl_array'
7
+ require_relative 'peruby/runtime/perl_hash'
8
+ require_relative 'peruby/runtime/glob'
9
+ require_relative 'peruby/runtime/ref'
10
+ require_relative 'peruby/runtime/conv'
11
+ require_relative 'peruby/runtime/stash'
12
+ require_relative 'peruby/runtime/local_stack'
13
+ require_relative 'peruby/runtime/sprintf'
14
+ require_relative 'peruby/runtime/regexp_compiler'
15
+ require_relative 'peruby/runtime/match_state'
16
+ require_relative 'peruby/runtime/mro'
17
+ require_relative 'peruby/runtime/io_handle'
18
+ require_relative 'peruby/runtime/directory_handle'
19
+ require_relative 'peruby/runtime/module_loader'
20
+ require_relative 'peruby/runtime/test_builder'
21
+ require_relative 'peruby/op'
22
+ require_relative 'peruby/lexer'
23
+ require_relative 'peruby/node'
24
+ require_relative 'peruby/parser'
25
+ require_relative 'peruby/validator'
26
+ require_relative 'peruby/runtime'
27
+ require_relative 'peruby/compiler'
28
+ require_relative 'peruby/compile_unit'
29
+
30
+ # Pure Ruby implementation of a Perl 5 interpreter.
31
+ module Peruby; end
data/t/00-basic.t ADDED
@@ -0,0 +1,5 @@
1
+ use MiniTest qw(ok is done_testing);
2
+
3
+ ok(1, "truth");
4
+ is(2 + 3, 5, "arithmetic");
5
+ done_testing();
data/t/lib/MiniTest.pm ADDED
@@ -0,0 +1,22 @@
1
+ package MiniTest;
2
+
3
+ our $count = 0;
4
+
5
+ sub ok {
6
+ my ($value, $name) = @_;
7
+ $count += 1;
8
+ print($value ? "ok " : "not ok ", $count, defined($name) ? " - $name" : "", "\n");
9
+ return $value ? 1 : 0;
10
+ }
11
+
12
+ sub is {
13
+ my ($got, $expected, $name) = @_;
14
+ return ok(defined($got) && defined($expected) && $got eq $expected, $name);
15
+ }
16
+
17
+ sub done_testing {
18
+ print "1..$count\n";
19
+ return 1;
20
+ }
21
+
22
+ 1;