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
data/lib/peruby/cli.rb
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'pp'
|
|
5
|
+
|
|
6
|
+
module Peruby
|
|
7
|
+
# Command-line entry point for the interpreter.
|
|
8
|
+
class CLI
|
|
9
|
+
def self.run(argv, input: $stdin, out: $stdout, err: $stderr)
|
|
10
|
+
new(argv, input:, out:, err:).run
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(argv, input:, out:, err:)
|
|
14
|
+
@argv = argv
|
|
15
|
+
@input = input
|
|
16
|
+
@out = out
|
|
17
|
+
@err = err
|
|
18
|
+
@debug = ENV.fetch('PERUBY_DEBUG', '').split(',')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
parser.order!(@argv)
|
|
23
|
+
return 0 if @shown_version
|
|
24
|
+
return dump_tokens if @dump_tokens
|
|
25
|
+
return dump_ast if @dump_ast
|
|
26
|
+
return dump_ops if @dump_ops
|
|
27
|
+
|
|
28
|
+
execute
|
|
29
|
+
rescue OptionParser::ParseError => e
|
|
30
|
+
@err.puts e.message
|
|
31
|
+
1
|
|
32
|
+
rescue CompileError => e
|
|
33
|
+
@err.puts e.message
|
|
34
|
+
@err.puts "Execution of #{@file} aborted due to compilation errors." if @file
|
|
35
|
+
1
|
|
36
|
+
rescue PerlExit => e
|
|
37
|
+
status = @runtime && @runtime.stash.glob('?').scalar.get
|
|
38
|
+
status.nil? ? e.status : Conv.to_num(status).to_i
|
|
39
|
+
rescue PerlError => e
|
|
40
|
+
@err.print(e.formatted? ? e.message : @runtime.error_message(e.message, file: @file))
|
|
41
|
+
1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength
|
|
47
|
+
def parser
|
|
48
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
49
|
+
OptionParser.new do |options|
|
|
50
|
+
options.banner = 'Usage: peruby [options] [programfile]'
|
|
51
|
+
options.on('-v', '--version', 'Print the version') do
|
|
52
|
+
@out.puts "peruby #{VERSION}"
|
|
53
|
+
@shown_version = true
|
|
54
|
+
end
|
|
55
|
+
options.on('--dump-tokens', 'Print lexer tokens') { @dump_tokens = true }
|
|
56
|
+
options.on('--dump-ast', 'Print the syntax tree') { @dump_ast = true }
|
|
57
|
+
options.on('--dump-ops', 'Print the operation tree') { @dump_ops = true }
|
|
58
|
+
options.on('--trace-ops', 'Trace operation execution') do
|
|
59
|
+
@trace_ops = true
|
|
60
|
+
@interpreter = :tree
|
|
61
|
+
end
|
|
62
|
+
options.on('-e CODE', 'Execute one line of program') { |code| @code = code }
|
|
63
|
+
options.on('-E CODE', 'Execute one line with features enabled') { |code| @code = code }
|
|
64
|
+
options.on('-c', 'Check syntax only') { @check = true }
|
|
65
|
+
options.on('-n', 'Run the program for each input record') { @loop = true }
|
|
66
|
+
options.on('-p', 'Run and print each input record') { @loop = @print_loop = true }
|
|
67
|
+
options.on('-l', 'Chomp input and append a newline to print') { @line_end = true }
|
|
68
|
+
options.on('-a', 'Autosplit input into @F') { @autosplit = true }
|
|
69
|
+
options.on('-F PATTERN', 'Autosplit pattern') do |pattern|
|
|
70
|
+
@split_pattern = pattern
|
|
71
|
+
@autosplit = true
|
|
72
|
+
end
|
|
73
|
+
options.on('-0[OCTAL]', 'Set the input record separator') { |octal| @separator = separator(octal) }
|
|
74
|
+
options.on('-M MODULE', 'Use a module before the program') { |name| (@use_modules ||= []) << name }
|
|
75
|
+
options.on('-m MODULE', 'Load a module without imports') { |name| (@load_modules ||= []) << name }
|
|
76
|
+
options.on('-s', 'Set script switches as package variables') { @script_switches = true }
|
|
77
|
+
options.on('-I DIRECTORY', 'Add a module search path') { |path| (@include_paths ||= []) << path }
|
|
78
|
+
options.on('--interpreter MODE', %w[compiled tree], 'Select compiled or tree execution') do |mode|
|
|
79
|
+
@interpreter = mode.to_sym
|
|
80
|
+
end
|
|
81
|
+
options.on('--refcount', 'Run DESTROY when tracked references are released') { @refcount = true }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def dump_tokens
|
|
86
|
+
@file = @argv.shift or raise CompileError, 'no program file supplied'
|
|
87
|
+
Lexer.new(File.read(@file), file: @file).each { |token| @out.puts token }
|
|
88
|
+
0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def dump_ast
|
|
92
|
+
@file = @argv.shift or raise CompileError, 'no program file supplied'
|
|
93
|
+
@out.puts Parser.parse(File.read(@file), file: @file).to_sexp.inspect
|
|
94
|
+
0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def dump_ops
|
|
98
|
+
@file = @argv.shift or raise CompileError, 'no program file supplied'
|
|
99
|
+
@runtime = Runtime.new(stdin: @input, stdout: @out, stderr: @err)
|
|
100
|
+
PP.pp(CompileUnit.new(runtime: @runtime, file: @file, interpreter: :tree).compile(File.read(@file)), @out)
|
|
101
|
+
0
|
|
102
|
+
ensure
|
|
103
|
+
@runtime&.shutdown
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def execute
|
|
107
|
+
@file = @code ? '-e' : @argv.shift
|
|
108
|
+
raise CompileError, 'no program supplied' unless @file
|
|
109
|
+
|
|
110
|
+
source = module_prelude + (@code || File.read(@file))
|
|
111
|
+
debug_source(source)
|
|
112
|
+
@runtime = Runtime.new(stdin: @input, stdout: @out, stderr: @err, refcount: @refcount)
|
|
113
|
+
configure_runtime
|
|
114
|
+
unit = CompileUnit.new(runtime: @runtime, file: @file, interpreter: @interpreter || :compiled)
|
|
115
|
+
if @check
|
|
116
|
+
unit.compile(source)
|
|
117
|
+
@out.puts "#{@file} syntax OK"
|
|
118
|
+
else
|
|
119
|
+
operation = unit.compile(source)
|
|
120
|
+
@runtime.run_phase(:init)
|
|
121
|
+
run_operation(operation)
|
|
122
|
+
end
|
|
123
|
+
0
|
|
124
|
+
ensure
|
|
125
|
+
@runtime&.shutdown
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def debug_source(source)
|
|
129
|
+
Lexer.new(source, file: @file).each { |token| @err.puts "lexer #{token}" } if @debug.include?('lexer')
|
|
130
|
+
@err.puts "parser #{Parser.parse(source, file: @file).to_sexp.inspect}" if @debug.include?('parser')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def run_operation(operation)
|
|
134
|
+
execute = -> { @loop ? run_input_loop(operation) : operation.run(@runtime.env, :void) }
|
|
135
|
+
return execute.call unless @trace_ops
|
|
136
|
+
|
|
137
|
+
trace = TracePoint.new(:call) do |event|
|
|
138
|
+
next unless event.method_id == :run && event.self.is_a?(Op::Base)
|
|
139
|
+
|
|
140
|
+
@err.puts "run #{event.self.class.name.delete_prefix('Peruby::Op::')}"
|
|
141
|
+
end
|
|
142
|
+
trace.enable(&execute)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def configure_runtime
|
|
146
|
+
Array(@include_paths).reverse_each do |path|
|
|
147
|
+
@runtime.stash.glob('INC').array.cells.unshift(Scalar.new(path))
|
|
148
|
+
end
|
|
149
|
+
Array(@load_modules).each { |name| load_module(name) }
|
|
150
|
+
configure_arguments
|
|
151
|
+
@runtime.stash.glob('/').scalar.set(record_separator)
|
|
152
|
+
@runtime.stash.glob('\\').scalar.set("\n") if @line_end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def configure_arguments
|
|
156
|
+
if @script_switches
|
|
157
|
+
@argv.delete_if do |argument|
|
|
158
|
+
next false unless argument.match?(/\A-[A-Za-z_]\w*(?:=.*)?\z/)
|
|
159
|
+
|
|
160
|
+
name, value = argument.delete_prefix('-').split('=', 2)
|
|
161
|
+
@runtime.stash.glob(name).scalar.set(value || 1)
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
cells = @argv.map { |argument| Scalar.new(argument) }
|
|
166
|
+
@runtime.stash.glob('ARGV').array.cells.replace(cells)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def load_module(name)
|
|
170
|
+
if Op::Use::CORE.include?(name)
|
|
171
|
+
@runtime.stash.glob('INC').hash.slot("#{name.gsub('::', '/')}.pm").set('(peruby internal)')
|
|
172
|
+
else
|
|
173
|
+
@runtime.module_loader.require_module(name)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def module_prelude
|
|
178
|
+
Array(@use_modules).map do |specification|
|
|
179
|
+
name, imports = specification.split('=', 2)
|
|
180
|
+
imports ? "use #{name} qw(#{imports.tr(',', ' ')});\n" : "use #{name};\n"
|
|
181
|
+
end.join
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def run_input_loop(operation)
|
|
185
|
+
each_input_record do |record|
|
|
186
|
+
record = record.delete_suffix(record_separator.to_s) if @line_end && record_separator
|
|
187
|
+
@runtime.stash.glob('_').scalar.set(record)
|
|
188
|
+
autosplit(record) if @autosplit
|
|
189
|
+
operation.run(@runtime.env, :void)
|
|
190
|
+
@out.print(Conv.to_str(@runtime.stash.glob('_').scalar.get), @line_end ? "\n" : '') if @print_loop
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def each_input_record(&block)
|
|
195
|
+
return each_record(@input, &block) if @argv.empty?
|
|
196
|
+
|
|
197
|
+
@argv.each do |path|
|
|
198
|
+
File.open(path) { |input| each_record(input, &block) }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def each_record(input, &block)
|
|
203
|
+
return block.call(input.read) unless record_separator
|
|
204
|
+
|
|
205
|
+
input.each_line(record_separator, &block)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def autosplit(record)
|
|
209
|
+
fields = @split_pattern ? record.split(RegexpCompiler.compile(@split_pattern)) : record.split
|
|
210
|
+
@runtime.stash.glob('F').array.cells.replace(fields.map { |field| Scalar.new(field) })
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def record_separator
|
|
214
|
+
defined?(@separator) ? @separator : "\n"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def separator(octal)
|
|
218
|
+
value = octal || '0'
|
|
219
|
+
return nil if value == '777'
|
|
220
|
+
|
|
221
|
+
value.to_i(8).chr
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# One parse/compile/run unit.
|
|
5
|
+
class CompileUnit
|
|
6
|
+
attr_reader :runtime, :file
|
|
7
|
+
|
|
8
|
+
def initialize(runtime: Runtime.new, file: '-e', interpreter: :compiled)
|
|
9
|
+
@runtime = runtime
|
|
10
|
+
@file = file
|
|
11
|
+
@interpreter = interpreter
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile(source, env: nil)
|
|
15
|
+
@runtime.with_file(@file) do
|
|
16
|
+
previous_env = @compile_env
|
|
17
|
+
@compile_env = (env || @runtime.env).fork
|
|
18
|
+
@compile_scopes = [{}]
|
|
19
|
+
@predeclared = {}.compare_by_identity
|
|
20
|
+
strict = @compile_env.strict_categories
|
|
21
|
+
warnings = @runtime.warning_categories
|
|
22
|
+
tree = Parser.parse(source, file: @file, known_subs: known_subs(@compile_env.package), unit: self,
|
|
23
|
+
package: @compile_env.package)
|
|
24
|
+
Validator.validate(tree, runtime: @runtime, strict:, warnings:)
|
|
25
|
+
operation = compiler.compile(tree)
|
|
26
|
+
@runtime.run_phase(:check)
|
|
27
|
+
@interpreter == :tree ? operation : Op::Compiled.new(operation.to_callable, operation.to_subroutine_callable)
|
|
28
|
+
ensure
|
|
29
|
+
@compile_env = previous_env
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run(source, context: :void)
|
|
34
|
+
@runtime.with_file(@file) do
|
|
35
|
+
operation = compile(source)
|
|
36
|
+
@runtime.run_phase(:init)
|
|
37
|
+
@runtime.local_stack.within { operation.run(@runtime.env, context) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def register_phase(kind, block, _token, package)
|
|
42
|
+
Validator.validate(block, runtime: @runtime, strict: @compile_env&.strict_categories || Set.new,
|
|
43
|
+
warnings: @runtime.warning_categories)
|
|
44
|
+
operation = Op::PackageBlock.new(package, compiler.compile(block))
|
|
45
|
+
env = phase_env
|
|
46
|
+
return operation.run(env, :void) if kind == :begin
|
|
47
|
+
|
|
48
|
+
@runtime.register_phase(kind, operation, env:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run_at_compile_time(node)
|
|
52
|
+
env = node.is_a?(Node::Use) ? @compile_env : phase_env
|
|
53
|
+
compiler.compile(node).run(env, :void)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def enter_compile_scope
|
|
57
|
+
@compile_scopes << {}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def leave_compile_scope
|
|
61
|
+
@compile_scopes.pop
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def predeclare(node)
|
|
65
|
+
# perlmod: BEGIN runs immediately and can see lexicals declared before it.
|
|
66
|
+
variables = node.variable.is_a?(Node::Variable) ? [node.variable] : node.variable.items
|
|
67
|
+
values = variables.map { |variable| empty_value(variable.sigil) }
|
|
68
|
+
variables.zip(values) do |variable, value|
|
|
69
|
+
@compile_scopes.last[[variable.sigil, variable.name]] = value if value
|
|
70
|
+
end
|
|
71
|
+
@predeclared[node] = values
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def predeclare_variable(variable)
|
|
75
|
+
@compile_scopes.last[[variable.sigil, variable.name]] = empty_value(variable.sigil)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def compiler = Compiler.new(predeclared: @predeclared)
|
|
81
|
+
|
|
82
|
+
def phase_env
|
|
83
|
+
@compile_env.fork.tap do |env|
|
|
84
|
+
@compile_scopes.each do |scope|
|
|
85
|
+
scope.each { |(sigil, name), value| env.bind(sigil, name, value) }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def empty_value(sigil)
|
|
91
|
+
{ scalar: Scalar, array: PerlArray, hash: PerlHash }[sigil]&.new
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def known_subs(package)
|
|
95
|
+
@runtime.stash.symbols(package).filter_map do |name, glob|
|
|
96
|
+
next unless glob.code
|
|
97
|
+
|
|
98
|
+
prototype = glob.code.prototype
|
|
99
|
+
[name, prototype&.start_with?('&') ? :listop : prototype]
|
|
100
|
+
end.to_h
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'op/literal'
|
|
4
|
+
require_relative 'op/variable'
|
|
5
|
+
require_relative 'op/expression'
|
|
6
|
+
require_relative 'op/assign'
|
|
7
|
+
require_relative 'op/control'
|
|
8
|
+
require_relative 'op/list'
|
|
9
|
+
require_relative 'op/io'
|
|
10
|
+
require_relative 'op/call'
|
|
11
|
+
require_relative 'op/loop'
|
|
12
|
+
require_relative 'op/element'
|
|
13
|
+
require_relative 'op/regexp'
|
|
14
|
+
require_relative 'op/builtin'
|
|
15
|
+
|
|
16
|
+
module Peruby
|
|
17
|
+
# Converts immutable syntax nodes into executable operation trees.
|
|
18
|
+
class Compiler
|
|
19
|
+
def initialize(predeclared: nil)
|
|
20
|
+
@predeclared = predeclared
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def compile(node)
|
|
24
|
+
name = node.class.name.split('::').last.gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2')
|
|
25
|
+
.gsub(/([a-z\d])([A-Z])/, '\\1_\\2').downcase
|
|
26
|
+
method = "compile_#{name}"
|
|
27
|
+
raise CompileError, "Unsupported syntax node #{node.class}" unless respond_to?(method, true)
|
|
28
|
+
|
|
29
|
+
send(method, node)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def compile_program(node) = Op::Sequence.new(node.statements.map { |item| compile(item) })
|
|
35
|
+
def compile_block(node) = Op::Sequence.new(node.statements.map { |item| compile(item) }, scoped: true)
|
|
36
|
+
def compile_bare_block(node) = Op::BareBlock.new(compile(node.body), node.label)
|
|
37
|
+
def compile_nop(_node) = Op::Literal.new(nil)
|
|
38
|
+
def compile_literal(node) = Op::Literal.new(node.value)
|
|
39
|
+
def compile_bareword(node) = Op::Literal.new(node.value)
|
|
40
|
+
def compile_variable(node) = Op::Variable.new(node.sigil, node.name)
|
|
41
|
+
|
|
42
|
+
def compile_dereference(node)
|
|
43
|
+
operation = raw_dereference(node)
|
|
44
|
+
node.kind == :code ? Op::CodeCall.new(operation, Op::List.new([]), inherit: true) : operation
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def compile_dereference_expr(node) = Op::DereferenceExpression.new(node.kind, compile(node.expression))
|
|
48
|
+
def compile_group(node) = Op::Group.new(compile(node.expression))
|
|
49
|
+
|
|
50
|
+
def compile_unary(node)
|
|
51
|
+
expression = if node.operator == :reference && node.expression.is_a?(Node::Dereference)
|
|
52
|
+
raw_dereference(node.expression)
|
|
53
|
+
else
|
|
54
|
+
compile(node.expression)
|
|
55
|
+
end
|
|
56
|
+
Op::Unary.new(node.operator, expression)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def compile_binary(node) = Op::Binary.new(node.operator, compile(node.left), compile(node.right))
|
|
60
|
+
|
|
61
|
+
def compile_ternary(node)
|
|
62
|
+
Op::Ternary.new(compile(node.condition), compile(node.true_expression), compile(node.false_expression))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def compile_assign(node) = Op::Assign.new(node.operator, compile(node.left), compile(node.right))
|
|
66
|
+
def compile_list(node) = Op::List.new(node.items.map { |item| compile(item) })
|
|
67
|
+
def compile_array_literal(node) = Op::ArrayLiteral.new(node.items.map { |item| compile(item) })
|
|
68
|
+
def compile_hash_literal(node) = Op::HashLiteral.new(node.items.map { |item| compile(item) })
|
|
69
|
+
def compile_match(node) = Op::Match.new(node.quote)
|
|
70
|
+
def compile_regexp(node) = Op::RegexpLiteral.new(node.quote)
|
|
71
|
+
def compile_bind(node) = Op::Bind.new(compile(node.subject), compile(node.pattern), node.negated)
|
|
72
|
+
def compile_substitute(node) = Op::Substitute.new(node.quote)
|
|
73
|
+
def compile_transliterate(node) = Op::Transliterate.new(node.quote)
|
|
74
|
+
def compile_readline(node) = Op::Readline.new(node.handle)
|
|
75
|
+
def compile_file_test(node) = Op::FileTest.new(node.operator, compile(node.expression))
|
|
76
|
+
def compile_postfix(node) = Op::Postfix.new(node.operator, compile(node.expression))
|
|
77
|
+
def compile_prefix(node) = Op::Prefix.new(node.operator, compile(node.expression))
|
|
78
|
+
def compile_modifier_for(node) = Op::ModifierFor.new(compile(node.expression), compile(node.list))
|
|
79
|
+
def compile_loop_jump(node) = Op::LoopJump.new(node.kind, node.label)
|
|
80
|
+
def compile_package(node) = Op::PackageSwitch.new(node.name)
|
|
81
|
+
def compile_package_block(node) = Op::PackageBlock.new(node.name, compile(node.body))
|
|
82
|
+
|
|
83
|
+
def compile_modifier_condition(node)
|
|
84
|
+
condition = compile(node.condition)
|
|
85
|
+
expression = compile(node.expression)
|
|
86
|
+
return Op::Conditional.new(condition, expression, nil) if node.kind == :if
|
|
87
|
+
return Op::Conditional.new(Op::Unary.new(:not, condition), expression, nil) if node.kind == :unless
|
|
88
|
+
|
|
89
|
+
Op::While.new(condition, expression, node.kind == :until, nil, nil)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def compile_map(node) = Op::Map.new(compile(node.body), compile(node.list), node.grep)
|
|
93
|
+
|
|
94
|
+
def compile_sort(node)
|
|
95
|
+
comparator = node.comparator && compile_sort_comparator(node.comparator)
|
|
96
|
+
Op::Sort.new(comparator, compile(node.list), node.package)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def compile_use(node) = Op::Use.new(node.module_name, node.imports, node.disable, node.package)
|
|
100
|
+
def compile_require(node) = Op::Require.new(compile(node.expression))
|
|
101
|
+
def compile_eval(node) = Op::Eval.new(compile(node.body), node.string, file: node.file)
|
|
102
|
+
def compile_constant(node) = Op::ConstantDef.new(node.name, compile(node.value))
|
|
103
|
+
|
|
104
|
+
def compile_my(node)
|
|
105
|
+
initializer = node.initializer && compile(node.initializer)
|
|
106
|
+
values = @predeclared&.[](node)
|
|
107
|
+
if node.variable.is_a?(Node::Variable)
|
|
108
|
+
return Op::Declare.new(node.variable.sigil, node.variable.name, initializer, values&.first)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
variables = node.variable.items.map { |item| [item.sigil, item.name] }
|
|
112
|
+
Op::DeclareList.new(variables, initializer, values)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def compile_state(node)
|
|
116
|
+
raise CompileError, 'state list declarations are not supported' unless node.variable.is_a?(Node::Variable)
|
|
117
|
+
|
|
118
|
+
value = @predeclared&.[](node)&.first
|
|
119
|
+
Op::State.new(node.variable.sigil, node.variable.name, node.initializer && compile(node.initializer), value)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def compile_our(node)
|
|
123
|
+
initializer = node.initializer && compile(node.initializer)
|
|
124
|
+
Op::GlobalDeclare.new(node.variable.sigil, node.variable.name, initializer)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def compile_local(node)
|
|
128
|
+
target = compile(node.target)
|
|
129
|
+
assignment = node.initializer && Op::Assign.new(:'=', target, compile(node.initializer))
|
|
130
|
+
Op::Local.new(target, assignment)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def compile_if(node)
|
|
134
|
+
otherwise = node.else_block && compile(node.else_block)
|
|
135
|
+
Op::Conditional.new(compile(node.condition), compile(node.then_block), otherwise)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def compile_while(node)
|
|
139
|
+
condition = compile(node.condition)
|
|
140
|
+
condition = Op::ReadlineCondition.new(condition) if node.condition.is_a?(Node::Readline)
|
|
141
|
+
continuation = node.continue_block && compile(node.continue_block)
|
|
142
|
+
Op::While.new(condition, compile(node.body), node.until_loop, continuation, node.label)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def compile_c_for(node)
|
|
146
|
+
Op::CFor.new(node.initializer && compile(node.initializer), node.condition && compile(node.condition),
|
|
147
|
+
node.step && compile(node.step), compile(node.body),
|
|
148
|
+
node.continue_block && compile(node.continue_block), node.label)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def compile_print(node) = Op::Print.new(node.handle, node.arguments && compile(node.arguments))
|
|
152
|
+
def compile_printf(node) = Op::Printf.new(node.handle, node.arguments && compile(node.arguments))
|
|
153
|
+
def compile_say(node) = Op::Say.new(node.handle, node.arguments && compile(node.arguments))
|
|
154
|
+
def compile_sub_def(node) = Op::SubDef.new(node.name, node.prototype, compile_sub_body(node.body))
|
|
155
|
+
def compile_anon_sub(node) = Op::AnonSub.new(node.prototype, compile_sub_body(node.body))
|
|
156
|
+
|
|
157
|
+
def compile_sub_body(block)
|
|
158
|
+
Op::Sequence.new(block.statements.map { |statement| compile(statement) })
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
162
|
+
def compile_call(node)
|
|
163
|
+
return compile_split(node) if node.name == 'split'
|
|
164
|
+
return Op::RefKind.new(compile(node.arguments.first)) if node.name == 'ref'
|
|
165
|
+
if node.name == 'push'
|
|
166
|
+
return Op::Push.new(compile(node.arguments.first), node.arguments.drop(1).map { |item| compile(item) })
|
|
167
|
+
end
|
|
168
|
+
return Op::Exists.new(compile(node.arguments.first)) if node.name == 'exists'
|
|
169
|
+
return Op::Delete.new(compile(node.arguments.first)) if node.name == 'delete'
|
|
170
|
+
return Op::Bless.new(node.arguments.map { |item| compile(item) }) if node.name == 'bless'
|
|
171
|
+
return Op::Undef.new(compile(node.arguments.first)) if node.name == 'undef'
|
|
172
|
+
|
|
173
|
+
builtin_name = node.name.rpartition('::').last
|
|
174
|
+
builtin_module = node.name.rpartition('::').first
|
|
175
|
+
core_builtin = builtin_module.empty? || ModuleLoader::INTERNAL_MODULES.include?(builtin_module)
|
|
176
|
+
if core_builtin && Op::Builtin.supports?(builtin_name)
|
|
177
|
+
arguments = node.arguments.map { |item| compile(item) }
|
|
178
|
+
return Op::Builtin.new(builtin_name, arguments, file: node.file, line: node.line)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
Op::Call.new(node.name, Op::List.new(node.arguments.map { |item| compile(item) }))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def compile_split(node)
|
|
185
|
+
arguments = node.arguments.map do |item|
|
|
186
|
+
item.is_a?(Node::Match) ? Op::RegexpLiteral.new(item.quote) : compile(item)
|
|
187
|
+
end
|
|
188
|
+
Op::Split.new(arguments)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def compile_code_call(node)
|
|
192
|
+
Op::CodeCall.new(compile(node.receiver), Op::List.new(node.arguments.map { |item| compile(item) }))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def compile_method_call(node)
|
|
196
|
+
name = node.name.is_a?(Node::Variable) ? compile(node.name) : node.name
|
|
197
|
+
arguments = Op::List.new(node.arguments.map { |item| compile(item) })
|
|
198
|
+
Op::MethodCall.new(compile(node.receiver), name, arguments)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def compile_return(node) = Op::Return.new(node.expression && compile(node.expression))
|
|
202
|
+
def compile_goto(node) = Op::Goto.new(node.name)
|
|
203
|
+
|
|
204
|
+
def compile_for_each(node)
|
|
205
|
+
continuation = node.continue_block && compile(node.continue_block)
|
|
206
|
+
Op::ForEach.new(node.variable.sigil, node.variable.name, compile(node.list), compile(node.body), continuation,
|
|
207
|
+
node.label)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def compile_element(node)
|
|
211
|
+
slice = (node.container.respond_to?(:sigil) && node.container.sigil != :scalar) ||
|
|
212
|
+
([Node::Dereference, Node::DereferenceExpr].any? { |type| node.container.is_a?(type) } &&
|
|
213
|
+
%i[array hash].include?(node.container.kind))
|
|
214
|
+
key_value = node.container.respond_to?(:sigil) && node.container.sigil == :hash
|
|
215
|
+
Op::Element.new(compile(node.container), compile(node.key), node.kind, node.dereference, slice:, key_value:)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def raw_dereference(node) = Op::Dereference.new(node.kind, node.name)
|
|
219
|
+
|
|
220
|
+
def compile_sort_comparator(comparator)
|
|
221
|
+
comparator.is_a?(String) ? comparator : compile(comparator)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class CompileError < Error; end
|
|
6
|
+
|
|
7
|
+
# Internal non-local control transfer for loop operators.
|
|
8
|
+
class LoopControl < StandardError
|
|
9
|
+
attr_reader :kind, :label
|
|
10
|
+
|
|
11
|
+
def initialize(kind, label = nil)
|
|
12
|
+
@kind = kind
|
|
13
|
+
@label = label
|
|
14
|
+
super([kind, label].compact.join(' '))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Non-error control flow for Perl's exit builtin.
|
|
19
|
+
class PerlExit < StandardError
|
|
20
|
+
attr_reader :status
|
|
21
|
+
|
|
22
|
+
def initialize(status)
|
|
23
|
+
@status = status
|
|
24
|
+
super("exit #{status}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Runtime exception retaining Perl's scalar error value.
|
|
29
|
+
class PerlError < Error
|
|
30
|
+
attr_reader :value
|
|
31
|
+
|
|
32
|
+
def initialize(message = nil, value: nil, formatted: false)
|
|
33
|
+
@value = value.nil? ? message : value
|
|
34
|
+
@formatted = formatted
|
|
35
|
+
super(message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def formatted? = @formatted
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
class Lexer
|
|
5
|
+
# Perl keywords grouped by their grammar role.
|
|
6
|
+
module Keywords
|
|
7
|
+
RESERVED = %w[
|
|
8
|
+
BEGIN CHECK END INIT UNITCHECK and break cmp continue CORE default do else elsif eq eval evalbytes for foreach
|
|
9
|
+
format ge given goto gt if last le local lt m my ne next no or our package q qq qr qw qx redo require return s
|
|
10
|
+
state sub tr unless until use when while xor y
|
|
11
|
+
].to_h { |word| [word, word.upcase.to_sym] }.freeze
|
|
12
|
+
|
|
13
|
+
NAMED_UNARY = %w[
|
|
14
|
+
abs chdir chomp chop chr chroot cos defined delete each eof eval exists exp fc fileno getc gethostbyname
|
|
15
|
+
getnetbyname getpgrp getprotobyname getprotobynumber getpwnam getpwuid getgrnam getgrgid hex int keys lc lcfirst
|
|
16
|
+
length localtime
|
|
17
|
+
lock log lstat oct ord quotemeta rand readlink ref reverse rewinddir scalar sin sqrt stat study uc ucfirst umask
|
|
18
|
+
undef values write
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
LISTOP = %w[
|
|
22
|
+
accept alarm all any atan2 bind binmode bless caller chmod chown close closedir connect crypt dbmclose dbmopen
|
|
23
|
+
die dump exec exit fcntl first flock fork formline gethostbyaddr getnetbyaddr getpeername getpriority
|
|
24
|
+
getservbyname getservbyport getsockname getsockopt glob gmtime grep import index ioctl join kill link listen
|
|
25
|
+
map mkdir msgctl msgget msgrcv msgsnd none open opendir pack pipe pop pos print printf prototype push read
|
|
26
|
+
readdir reduce
|
|
27
|
+
readline readpipe recv rename reset rindex rmdir say seek seekdir select semctl semget semop send setpgrp
|
|
28
|
+
setpriority
|
|
29
|
+
setsockopt shift shmctl shmget shmread shmwrite shutdown sleep socket socketpair sort splice split sprintf srand
|
|
30
|
+
substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time truncate unlink
|
|
31
|
+
unpack unshift untie utime vec wait waitpid wantarray warn
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
FUNC0 = %w[
|
|
35
|
+
caller endgrent endhostent endnetent endprotoent endpwent endservent eof getgrent gethostent getlogin getnetent
|
|
36
|
+
getprotoent getppid getpwent getservent setgrent sethostent setnetent setprotoent setpwent setservent time times
|
|
37
|
+
wantarray
|
|
38
|
+
].freeze
|
|
39
|
+
SPECIAL_VARIABLES = ['_', '@', '!', '/', '\\', ',', '"', ';', '.', '0', '?', '$', '|', '^W', '^O', ']', '[',
|
|
40
|
+
'+', '-', '`', "'", '&'].freeze
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def type(word, expect)
|
|
45
|
+
return :CONSTANT if word == 'constant'
|
|
46
|
+
return :NOT if word == 'not'
|
|
47
|
+
return operator_word(word) if expect == :operator && operator_word(word)
|
|
48
|
+
return word.upcase.to_sym if %w[print printf say sort map grep].include?(word)
|
|
49
|
+
return RESERVED[word] if RESERVED.key?(word)
|
|
50
|
+
return :NAMED_UNARY if NAMED_UNARY.include?(word)
|
|
51
|
+
return :LISTOP if LISTOP.include?(word)
|
|
52
|
+
return :FUNC if FUNC0.include?(word)
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def operator_word(word)
|
|
58
|
+
{ 'x' => :REPEAT, 'lt' => :SLT, 'gt' => :SGT, 'le' => :SLE, 'ge' => :SGE,
|
|
59
|
+
'eq' => :SEQ, 'ne' => :SNE, 'cmp' => :SCMP, 'and' => :AND, 'or' => :OR, 'xor' => :XOR }[word]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|