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.
- checksums.yaml +7 -0
- data/.rubocop.yml +35 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/Rakefile +92 -0
- data/bin/peruby +9 -0
- data/doc/COMPAT.md +54 -0
- data/doc/CONTRIBUTING.md +21 -0
- data/doc/DESIGN.md +25 -0
- data/doc/INCOMPATIBILITIES.md +30 -0
- data/doc/PERF.md +51 -0
- data/doc/ROADMAP.md +18 -0
- data/examples/hello.pl +1 -0
- data/examples/json.pl +2 -0
- data/examples/object.pl +5 -0
- data/examples/word_count.pl +6 -0
- data/lib/peruby/cli.rb +224 -0
- data/lib/peruby/compile_unit.rb +103 -0
- data/lib/peruby/compiler.rb +224 -0
- data/lib/peruby/errors.rb +40 -0
- data/lib/peruby/lexer/heredoc.rb +8 -0
- data/lib/peruby/lexer/keywords.rb +63 -0
- data/lib/peruby/lexer/number.rb +32 -0
- data/lib/peruby/lexer/quote_like.rb +72 -0
- data/lib/peruby/lexer/source_scanner.rb +104 -0
- data/lib/peruby/lexer/state.rb +46 -0
- data/lib/peruby/lexer/structure_scanner.rb +149 -0
- data/lib/peruby/lexer/term_scanner.rb +301 -0
- data/lib/peruby/lexer/token.rb +16 -0
- data/lib/peruby/lexer.rb +123 -0
- data/lib/peruby/node.rb +88 -0
- data/lib/peruby/op/assign.rb +128 -0
- data/lib/peruby/op/builtin.rb +903 -0
- data/lib/peruby/op/call.rb +378 -0
- data/lib/peruby/op/control.rb +256 -0
- data/lib/peruby/op/element.rb +136 -0
- data/lib/peruby/op/expression.rb +342 -0
- data/lib/peruby/op/io.rb +113 -0
- data/lib/peruby/op/list.rb +102 -0
- data/lib/peruby/op/literal.rb +84 -0
- data/lib/peruby/op/loop.rb +158 -0
- data/lib/peruby/op/regexp.rb +288 -0
- data/lib/peruby/op/variable.rb +534 -0
- data/lib/peruby/op.rb +47 -0
- data/lib/peruby/parser/grammar.rb +5797 -0
- data/lib/peruby/parser/grammar.y +576 -0
- data/lib/peruby/parser.rb +14 -0
- data/lib/peruby/runtime/code.rb +21 -0
- data/lib/peruby/runtime/conv.rb +140 -0
- data/lib/peruby/runtime/directory_handle.rb +18 -0
- data/lib/peruby/runtime/env.rb +129 -0
- data/lib/peruby/runtime/glob.rb +24 -0
- data/lib/peruby/runtime/interpolation.rb +223 -0
- data/lib/peruby/runtime/io_handle.rb +37 -0
- data/lib/peruby/runtime/local_stack.rb +90 -0
- data/lib/peruby/runtime/match_state.rb +62 -0
- data/lib/peruby/runtime/module_loader.rb +133 -0
- data/lib/peruby/runtime/mro.rb +94 -0
- data/lib/peruby/runtime/perl_array.rb +81 -0
- data/lib/peruby/runtime/perl_hash.rb +57 -0
- data/lib/peruby/runtime/ref.rb +43 -0
- data/lib/peruby/runtime/regexp_compiler.rb +75 -0
- data/lib/peruby/runtime/scalar.rb +27 -0
- data/lib/peruby/runtime/sprintf.rb +54 -0
- data/lib/peruby/runtime/stash.rb +50 -0
- data/lib/peruby/runtime/test_builder.rb +47 -0
- data/lib/peruby/runtime.rb +325 -0
- data/lib/peruby/validator.rb +236 -0
- data/lib/peruby/version.rb +5 -0
- data/lib/peruby.rb +31 -0
- data/t/00-basic.t +5 -0
- data/t/lib/MiniTest.pm +22 -0
- 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
|
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
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;
|