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
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ class Lexer
5
+ # Mutable payload filled when the lexer reaches the line after a heredoc declaration.
6
+ Heredoc = Struct.new(:terminator, :body, :interpolate, :indent, keyword_init: true)
7
+ end
8
+ 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