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,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Dynamic values exposed by Perl's most recent successful regexp match.
|
|
5
|
+
class MatchState
|
|
6
|
+
attr_reader :md, :subject
|
|
7
|
+
|
|
8
|
+
def initialize(match_data = nil, subject = '')
|
|
9
|
+
@md = match_data
|
|
10
|
+
@subject = subject
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fetch(sigil, name)
|
|
14
|
+
return nil unless @md
|
|
15
|
+
return scalar(name) if sigil == :scalar && name.match?(/\A(?:\d+|[&`'+])\z/)
|
|
16
|
+
return offsets(name) if sigil == :array && %w[- +].include?(name)
|
|
17
|
+
return named(name) if sigil == :hash && %w[- +].include?(name)
|
|
18
|
+
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def scalar(name)
|
|
25
|
+
value = case name
|
|
26
|
+
when '&' then @md&.[](0)
|
|
27
|
+
when '`' then @md && @subject[0...@md.begin(0)]
|
|
28
|
+
when "'" then @md && @subject[@md.end(0)..]
|
|
29
|
+
when '+' then last_capture
|
|
30
|
+
else @md&.[](name.to_i)
|
|
31
|
+
end
|
|
32
|
+
Scalar.new(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def last_capture
|
|
36
|
+
@md && @md.captures.reverse.find { |capture| !capture.nil? }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def offsets(name)
|
|
40
|
+
positions = return_positions(name == '-' ? :begin : :end)
|
|
41
|
+
PerlArray.new(positions.map { |position| Scalar.new(position) })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def return_positions(method)
|
|
45
|
+
return [] unless @md
|
|
46
|
+
|
|
47
|
+
(0...@md.length).map { |index| @md.public_send(method, index) || -1 }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def named(name)
|
|
51
|
+
hash = PerlHash.new
|
|
52
|
+
return hash unless @md
|
|
53
|
+
|
|
54
|
+
@md.names.each do |key|
|
|
55
|
+
captures = @md.regexp.named_captures.fetch(key).map { |index| @md[index] }
|
|
56
|
+
values = name == '+' ? [captures.reverse.find { |value| !value.nil? }] : captures.compact
|
|
57
|
+
hash.slot(key).set(name == '+' ? values.first : PerlArray.new(values.map { |value| Scalar.new(value) }))
|
|
58
|
+
end
|
|
59
|
+
hash
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Interpreter namespace.
|
|
4
|
+
module Peruby
|
|
5
|
+
# Definition collected by Peruby.define_module's small Ruby DSL.
|
|
6
|
+
class ModuleDefinition
|
|
7
|
+
attr_reader :exports, :functions
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@exports = []
|
|
11
|
+
@functions = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def export_ok(*names) = @exports.concat(names.map(&:to_s))
|
|
15
|
+
|
|
16
|
+
def sub(name, proto: nil, &implementation)
|
|
17
|
+
@functions[name.to_s] = [proto, implementation]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def install(name, runtime)
|
|
21
|
+
@functions.each do |sub_name, (prototype, implementation)|
|
|
22
|
+
code = Code.new("#{name}::#{sub_name}", NativeBody.new(&implementation), runtime.env, name, prototype)
|
|
23
|
+
runtime.stash.glob(code.name).code = code
|
|
24
|
+
end
|
|
25
|
+
runtime.stash.touch
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Reentrant require/use loader with user @INC taking precedence.
|
|
30
|
+
class ModuleLoader
|
|
31
|
+
INTERNAL_MODULES = %w[
|
|
32
|
+
strict warnings utf8 feature integer bytes vars lib parent base Exporter constant Carp Scalar::Util List::Util
|
|
33
|
+
POSIX Data::Dumper Test::Builder Test::More Test::Simple File::Basename File::Spec File::Path File::Temp Cwd
|
|
34
|
+
Getopt::Long JSON::PP Encode Time::HiRes Digest::MD5 Digest::SHA Storable Fcntl Errno overload mro
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
@modules = {}
|
|
38
|
+
class << self
|
|
39
|
+
attr_reader :modules
|
|
40
|
+
|
|
41
|
+
def define(name, &block)
|
|
42
|
+
return @modules[name] = block unless block.arity.zero?
|
|
43
|
+
|
|
44
|
+
definition = ModuleDefinition.new
|
|
45
|
+
definition.instance_eval(&block)
|
|
46
|
+
@modules[name] = definition
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def prototype(module_name, sub_name)
|
|
50
|
+
implementation = @modules[module_name]
|
|
51
|
+
implementation.functions.dig(sub_name, 0) if implementation.is_a?(ModuleDefinition)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(runtime)
|
|
56
|
+
@runtime = runtime
|
|
57
|
+
@loading = []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def require_module(name)
|
|
61
|
+
key = filename(name)
|
|
62
|
+
return 1 if @runtime.stash.glob('INC').hash.exists?(key)
|
|
63
|
+
raise PerlError, "Recursive require of #{key}" if @loading.include?(key)
|
|
64
|
+
|
|
65
|
+
@loading << key
|
|
66
|
+
kind, content = find(key)
|
|
67
|
+
source = if kind == :path
|
|
68
|
+
load_file(content, key)
|
|
69
|
+
elsif kind == :source
|
|
70
|
+
load_source(content, key)
|
|
71
|
+
else
|
|
72
|
+
load_internal(name, key)
|
|
73
|
+
end
|
|
74
|
+
@runtime.stash.glob('INC').hash.slot(key).set(source)
|
|
75
|
+
1
|
|
76
|
+
ensure
|
|
77
|
+
@loading.delete(key) if key
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def filename(name)
|
|
83
|
+
value = Conv.to_str(name)
|
|
84
|
+
value.end_with?('.pl', '.pm') ? value : "#{value.gsub('::', '/')}.pm"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def find(filename)
|
|
88
|
+
@runtime.stash.glob('INC').array.cells.each do |cell|
|
|
89
|
+
value = cell.get
|
|
90
|
+
if value.is_a?(String)
|
|
91
|
+
path = File.expand_path(filename, value)
|
|
92
|
+
return [:path, path] if File.file?(path)
|
|
93
|
+
elsif value.is_a?(Ref) && value.kind == 'CODE'
|
|
94
|
+
source = @runtime.call(value.target, [cell, Scalar.new(filename)], :scalar)
|
|
95
|
+
source = source.target.get if source.is_a?(Ref) && source.target.is_a?(Scalar)
|
|
96
|
+
return [:source, Conv.to_str(source)] if source
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def load_file(path, key)
|
|
103
|
+
value = CompileUnit.new(runtime: @runtime, file: path).run(File.read(path), context: :scalar)
|
|
104
|
+
raise PerlError, "#{key} did not return a true value" unless Conv.truthy?(value)
|
|
105
|
+
|
|
106
|
+
path
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def load_source(source, key)
|
|
110
|
+
value = CompileUnit.new(runtime: @runtime, file: key).run(source, context: :scalar)
|
|
111
|
+
raise PerlError, "#{key} did not return a true value" unless Conv.truthy?(value)
|
|
112
|
+
|
|
113
|
+
'(INC hook)'
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def load_internal(name, key)
|
|
117
|
+
implementation = self.class.modules[Conv.to_str(name)]
|
|
118
|
+
return '(peruby internal)' if !implementation && INTERNAL_MODULES.include?(Conv.to_str(name))
|
|
119
|
+
raise PerlError, "Can't locate #{key} in @INC" unless implementation
|
|
120
|
+
|
|
121
|
+
if implementation.is_a?(ModuleDefinition)
|
|
122
|
+
implementation.install(Conv.to_str(name), @runtime)
|
|
123
|
+
else
|
|
124
|
+
implementation.call(@runtime)
|
|
125
|
+
end
|
|
126
|
+
'(peruby internal)'
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.define_module(name, &)
|
|
131
|
+
ModuleLoader.define(name, &)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Perl package method lookup over runtime @ISA values.
|
|
5
|
+
class MRO
|
|
6
|
+
def initialize(stash)
|
|
7
|
+
@stash = stash
|
|
8
|
+
@cache = {}
|
|
9
|
+
@modes = Hash.new(:dfs)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def set(package, mode)
|
|
13
|
+
raise PerlError, "Invalid MRO #{mode}" unless %i[dfs c3].include?(mode.to_sym)
|
|
14
|
+
|
|
15
|
+
@modes[package] = mode.to_sym
|
|
16
|
+
@cache.clear
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve(package, method, super_from: nil, autoload: true)
|
|
20
|
+
start = super_from ? parents(super_from) : [package]
|
|
21
|
+
order = start.flat_map { |candidate| linearized(candidate) }.uniq
|
|
22
|
+
order << 'UNIVERSAL' unless order.include?('UNIVERSAL')
|
|
23
|
+
found = order.filter_map { |candidate| method_entry(candidate, method) }.first
|
|
24
|
+
return found if found
|
|
25
|
+
return nil if method == 'DESTROY' || !autoload
|
|
26
|
+
|
|
27
|
+
autoload = order.filter_map { |candidate| method_entry(candidate, 'AUTOLOAD') }.first
|
|
28
|
+
if autoload
|
|
29
|
+
owner, = autoload
|
|
30
|
+
@stash.glob("#{owner}::AUTOLOAD").scalar.set("#{package}::#{method}")
|
|
31
|
+
end
|
|
32
|
+
autoload
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def isa?(package, parent)
|
|
36
|
+
package == parent || linearized(package).include?(parent)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def lineage(package) = linearized(package)
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def linearized(package)
|
|
44
|
+
key = [package, @modes[package], signature(package)]
|
|
45
|
+
@cache[key] ||= @modes[package] == :c3 ? c3(package) : linearize([package])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def signature(package, seen = {})
|
|
49
|
+
return package if seen[package]
|
|
50
|
+
|
|
51
|
+
seen[package] = true
|
|
52
|
+
[package, parents(package).map { |parent| signature(parent, seen) }]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def linearize(packages, seen = {})
|
|
56
|
+
packages.flat_map do |package|
|
|
57
|
+
next [] if seen[package]
|
|
58
|
+
|
|
59
|
+
seen[package] = true
|
|
60
|
+
[package, *linearize(parents(package), seen)]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def c3(package)
|
|
65
|
+
direct = parents(package)
|
|
66
|
+
[package, *merge(direct.map { |parent| c3(parent) } + [direct])]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def merge(sequences)
|
|
70
|
+
result = []
|
|
71
|
+
sequences = sequences.map(&:dup).reject(&:empty?)
|
|
72
|
+
until sequences.empty?
|
|
73
|
+
candidate = sequences.filter_map(&:first).find do |head|
|
|
74
|
+
sequences.none? { |sequence| sequence.drop(1).include?(head) }
|
|
75
|
+
end
|
|
76
|
+
raise PerlError, 'Inconsistent hierarchy during C3 merge' unless candidate
|
|
77
|
+
|
|
78
|
+
result << candidate
|
|
79
|
+
sequences.each { |sequence| sequence.shift if sequence.first == candidate }
|
|
80
|
+
sequences.reject!(&:empty?)
|
|
81
|
+
end
|
|
82
|
+
result
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parents(package)
|
|
86
|
+
@stash.glob("#{package}::ISA").array.cells.map { |cell| Conv.to_str(cell.get) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def method_entry(package, method)
|
|
90
|
+
code = @stash.fetch("#{package}::#{method}")&.code
|
|
91
|
+
code && [package, code]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Perl AV represented as an array of mutable scalar cells.
|
|
5
|
+
class PerlArray
|
|
6
|
+
attr_reader :cells
|
|
7
|
+
|
|
8
|
+
def initialize(cells = [])
|
|
9
|
+
@cells = cells
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(index)
|
|
13
|
+
index = normalize(index)
|
|
14
|
+
return nil if index.negative?
|
|
15
|
+
|
|
16
|
+
@cells[index]&.get
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def slot(index)
|
|
20
|
+
index = normalize(index)
|
|
21
|
+
raise PerlError, 'Modification of non-creatable array value' if index.negative?
|
|
22
|
+
|
|
23
|
+
@cells.fill(nil, @cells.length...index)
|
|
24
|
+
@cells[index] ||= Scalar.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def exists?(index)
|
|
28
|
+
index = normalize(index.to_i)
|
|
29
|
+
index >= 0 && index < @cells.length && !@cells[index].nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(index)
|
|
33
|
+
index = normalize(index.to_i)
|
|
34
|
+
return nil if index.negative? || index >= @cells.length
|
|
35
|
+
|
|
36
|
+
value = @cells[index]&.get
|
|
37
|
+
@cells[index] = nil
|
|
38
|
+
@cells.pop while @cells.last.nil?
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def size
|
|
43
|
+
@cells.size
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def list
|
|
47
|
+
@cells.each_index.map do |index|
|
|
48
|
+
@cells[index] || begin
|
|
49
|
+
cell = Scalar.new { @cells[index] = cell }
|
|
50
|
+
cell
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def last_index
|
|
56
|
+
size - 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def last_index=(index)
|
|
60
|
+
raise PerlError, 'Modification of non-creatable array value' if index < -1
|
|
61
|
+
|
|
62
|
+
target_size = index + 1
|
|
63
|
+
target_size < size ? @cells.slice!(target_size..) : @cells.fill(nil, size...target_size)
|
|
64
|
+
index
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def replace_cell(index, cell)
|
|
68
|
+
index = normalize(index)
|
|
69
|
+
raise PerlError, 'Modification of non-creatable array value' if index.negative?
|
|
70
|
+
|
|
71
|
+
@cells.fill(nil, size...index)
|
|
72
|
+
@cells[index] = cell
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def normalize(index)
|
|
78
|
+
index.negative? ? index + size : index
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Perl HV represented as string keys and mutable scalar cells.
|
|
5
|
+
class PerlHash
|
|
6
|
+
attr_reader :cells
|
|
7
|
+
|
|
8
|
+
def initialize(cells = {}, &on_set)
|
|
9
|
+
@cells = cells
|
|
10
|
+
@iterator = 0
|
|
11
|
+
@on_set = on_set
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fetch(key)
|
|
15
|
+
@cells[key.to_s]&.get
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def slot(key)
|
|
19
|
+
key = key.to_s
|
|
20
|
+
unless @cells.key?(key)
|
|
21
|
+
@iterator = 0
|
|
22
|
+
@cells[key] = Scalar.new { |value| @on_set&.call(key, value) }
|
|
23
|
+
end
|
|
24
|
+
@cells[key]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def exists?(key)
|
|
28
|
+
@cells.key?(key.to_s)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(key)
|
|
32
|
+
@iterator = 0
|
|
33
|
+
@cells.delete(key.to_s)&.get
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def keys
|
|
37
|
+
@iterator = 0
|
|
38
|
+
@cells.keys
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def each_pair
|
|
42
|
+
pair = @cells.to_a[@iterator]
|
|
43
|
+
@iterator = pair ? @iterator + 1 : 0
|
|
44
|
+
pair
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def list
|
|
48
|
+
@cells.flat_map { |key, cell| [Scalar.new(key), cell] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def replace_cell(key, cell)
|
|
52
|
+
@iterator = 0
|
|
53
|
+
key = key.to_s
|
|
54
|
+
cell ? @cells[key] = cell : @cells.delete(key)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'weakref'
|
|
4
|
+
|
|
5
|
+
module Peruby
|
|
6
|
+
# Perl reference with an optional blessed package.
|
|
7
|
+
class Ref
|
|
8
|
+
attr_accessor :blessed, :runtime
|
|
9
|
+
|
|
10
|
+
def initialize(target, blessed = nil)
|
|
11
|
+
@target = target
|
|
12
|
+
@blessed = blessed
|
|
13
|
+
@weak = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def target
|
|
17
|
+
@weak ? @target.__getobj__ : @target
|
|
18
|
+
rescue WeakRef::RefError
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def weaken!
|
|
23
|
+
@target = WeakRef.new(@target)
|
|
24
|
+
@weak = true
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def weak? = @weak
|
|
29
|
+
|
|
30
|
+
def kind
|
|
31
|
+
value = target
|
|
32
|
+
return 'REF' if value.is_a?(Scalar) && value.get.is_a?(Ref)
|
|
33
|
+
|
|
34
|
+
kinds = { Scalar => 'SCALAR', PerlArray => 'ARRAY', PerlHash => 'HASH', Glob => 'GLOB',
|
|
35
|
+
Regexp => 'Regexp', Code => 'CODE' }
|
|
36
|
+
kinds.fetch(value.class, nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ref_string
|
|
40
|
+
@blessed || kind
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Converts the Perl regexp differences that Ruby's Onigmo does not share.
|
|
5
|
+
module RegexpCompiler
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def compile(pattern, modifiers = '')
|
|
9
|
+
@cache ||= {}
|
|
10
|
+
@cache[[pattern, modifiers]] ||= Regexp.new(*transform(pattern, modifiers))
|
|
11
|
+
rescue RegexpError => e
|
|
12
|
+
raise CompileError, e.message
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def transform(pattern, modifiers = '')
|
|
16
|
+
raise CompileError, 'Regexp code execution is not supported' if pattern.match?(/\(\?\??\{/)
|
|
17
|
+
|
|
18
|
+
pattern = pattern.gsub(/\(\?\^([isx]*):/) { "(?#{Regexp.last_match(1).tr('s', 'm')}:" }
|
|
19
|
+
source = rewrite(pattern, multiline: modifiers.include?('m'))
|
|
20
|
+
source = extended_character_classes(source) if modifiers.include?('xx')
|
|
21
|
+
source = ascii_classes(source) if modifiers.include?('a')
|
|
22
|
+
options = 0
|
|
23
|
+
options |= Regexp::IGNORECASE if modifiers.include?('i')
|
|
24
|
+
options |= Regexp::EXTENDED if modifiers.include?('x')
|
|
25
|
+
options |= Regexp::MULTILINE if modifiers.include?('s')
|
|
26
|
+
[source, options]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rewrite(pattern, multiline:)
|
|
30
|
+
output = +''
|
|
31
|
+
character_class = false
|
|
32
|
+
escaped = false
|
|
33
|
+
pattern.each_char do |character|
|
|
34
|
+
if escaped
|
|
35
|
+
output << character
|
|
36
|
+
escaped = false
|
|
37
|
+
elsif character == '\\'
|
|
38
|
+
output << character
|
|
39
|
+
escaped = true
|
|
40
|
+
elsif character == '['
|
|
41
|
+
character_class = true
|
|
42
|
+
output << character
|
|
43
|
+
elsif character == ']' && character_class
|
|
44
|
+
character_class = false
|
|
45
|
+
output << character
|
|
46
|
+
elsif !character_class && !multiline && character == '^'
|
|
47
|
+
output << '\\A'
|
|
48
|
+
elsif !character_class && !multiline && character == '$'
|
|
49
|
+
output << '(?=\\n?\\z)'
|
|
50
|
+
else
|
|
51
|
+
output << character
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
output.gsub('(?R)', '\\g<0>').gsub(/\(\?(\d+)\)/, '\\\\g<\\1>')
|
|
55
|
+
end
|
|
56
|
+
private_class_method :rewrite
|
|
57
|
+
|
|
58
|
+
def extended_character_classes(pattern)
|
|
59
|
+
pattern.gsub(/\[(?:\\.|[^\]])*\]/) do |character_class|
|
|
60
|
+
character_class.gsub(/(?<!\\)\s+|(?<!\\)#.*?(?=\n|\])/, '')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
private_class_method :extended_character_classes
|
|
64
|
+
|
|
65
|
+
def ascii_classes(pattern)
|
|
66
|
+
pattern.gsub(/(?<!\\)((?:\\\\)*)\\([dDwWsS])/) do
|
|
67
|
+
prefix = Regexp.last_match(1)
|
|
68
|
+
replacement = { 'd' => '[0-9]', 'D' => '[^0-9]', 'w' => '[A-Za-z0-9_]', 'W' => '[^A-Za-z0-9_]',
|
|
69
|
+
's' => '[\\t\\n\\f\\r ]', 'S' => '[^\\t\\n\\f\\r ]' }.fetch(Regexp.last_match(2))
|
|
70
|
+
prefix + replacement
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
private_class_method :ascii_classes
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Mutable Perl scalar cell. Sharing a cell represents Perl aliasing.
|
|
5
|
+
class Scalar
|
|
6
|
+
attr_accessor :value, :pos
|
|
7
|
+
|
|
8
|
+
def initialize(value = nil, &on_set)
|
|
9
|
+
@value = value
|
|
10
|
+
@on_set = on_set
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
alias get value
|
|
14
|
+
|
|
15
|
+
def set(value)
|
|
16
|
+
@value = value
|
|
17
|
+
@on_set&.call(value)
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def copy
|
|
22
|
+
self.class.new(@value)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Dualvar = Data.define(:num, :str)
|
|
27
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Perl-compatible formatting facade with version-vector support.
|
|
5
|
+
module Sprintf
|
|
6
|
+
CONVERSION = /%(?:(\d+)\$)?([-+ 0#]*)(\*|\d+)?(?:\.(\*|\d+))?(v[diuoxXbB]?|[sdiuoxXbBeEfgGc%n])/
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
11
|
+
def format(template, values)
|
|
12
|
+
index = 0
|
|
13
|
+
template.gsub(CONVERSION) do |directive|
|
|
14
|
+
match = Regexp.last_match
|
|
15
|
+
conversion = match[5]
|
|
16
|
+
raise PerlError, '%n is not supported' if conversion == 'n'
|
|
17
|
+
next '%' if conversion == '%'
|
|
18
|
+
|
|
19
|
+
resolved = directive.dup
|
|
20
|
+
if match[3] == '*'
|
|
21
|
+
resolved.sub!('*', Conv.to_num(values[index]).to_i.to_s)
|
|
22
|
+
index += 1
|
|
23
|
+
end
|
|
24
|
+
if match[4] == '*'
|
|
25
|
+
resolved.sub!('*', Conv.to_num(values[index]).to_i.to_s)
|
|
26
|
+
index += 1
|
|
27
|
+
end
|
|
28
|
+
position = match[1] ? match[1].to_i - 1 : index
|
|
29
|
+
index += 1 unless match[1]
|
|
30
|
+
value = values.fetch(position, nil)
|
|
31
|
+
conversion.start_with?('v') ? version(value, conversion[1] || 'd') : ruby_format(resolved, value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def version(value, conversion)
|
|
36
|
+
Conv.to_str(value).codepoints.map { |point| Kernel.format("%#{conversion}", point) }.join('.')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ruby_format(directive, value)
|
|
40
|
+
conversion = directive[-1]
|
|
41
|
+
coerced = if conversion == 's'
|
|
42
|
+
Conv.to_str(value)
|
|
43
|
+
elsif conversion.match?(/[eEfgG]/)
|
|
44
|
+
Conv.to_num(value).to_f
|
|
45
|
+
else
|
|
46
|
+
integer = Conv.to_num(value).to_i
|
|
47
|
+
conversion.match?(/[uoxXbB]/) ? integer & ((1 << 64) - 1) : integer
|
|
48
|
+
end
|
|
49
|
+
Kernel.format(directive.sub(/%\d+\$/, '%'), coerced)
|
|
50
|
+
rescue ArgumentError, TypeError
|
|
51
|
+
''
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
# Rooted Perl package symbol table.
|
|
5
|
+
class Stash
|
|
6
|
+
attr_reader :generation
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@packages = Hash.new { |packages, name| packages[name] = {} }
|
|
10
|
+
@names = {}
|
|
11
|
+
@generation = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def glob(name)
|
|
15
|
+
package, symbol = split(name)
|
|
16
|
+
@packages[package][symbol] ||= begin
|
|
17
|
+
@generation += 1
|
|
18
|
+
Glob.new("#{package}::#{symbol}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch(name)
|
|
23
|
+
package, symbol = split(name)
|
|
24
|
+
@packages[package][symbol]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def symbols(package = 'main')
|
|
28
|
+
@packages[normalize_package(package)].dup
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def touch
|
|
32
|
+
@generation += 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def split(name)
|
|
38
|
+
value = name.to_s
|
|
39
|
+
return @names[value] if @names.key?(value)
|
|
40
|
+
|
|
41
|
+
parts = value.gsub("'", '::').split('::').reject(&:empty?)
|
|
42
|
+
symbol = parts.pop
|
|
43
|
+
@names[value] = [normalize_package(parts.join('::')), symbol]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize_package(package)
|
|
47
|
+
package.empty? || package == 'main' ? 'main' : package.delete_suffix('::')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|