peruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +35 -0
  3. data/CHANGELOG.md +15 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +51 -0
  6. data/Rakefile +92 -0
  7. data/bin/peruby +9 -0
  8. data/doc/COMPAT.md +54 -0
  9. data/doc/CONTRIBUTING.md +21 -0
  10. data/doc/DESIGN.md +25 -0
  11. data/doc/INCOMPATIBILITIES.md +30 -0
  12. data/doc/PERF.md +51 -0
  13. data/doc/ROADMAP.md +18 -0
  14. data/examples/hello.pl +1 -0
  15. data/examples/json.pl +2 -0
  16. data/examples/object.pl +5 -0
  17. data/examples/word_count.pl +6 -0
  18. data/lib/peruby/cli.rb +224 -0
  19. data/lib/peruby/compile_unit.rb +103 -0
  20. data/lib/peruby/compiler.rb +224 -0
  21. data/lib/peruby/errors.rb +40 -0
  22. data/lib/peruby/lexer/heredoc.rb +8 -0
  23. data/lib/peruby/lexer/keywords.rb +63 -0
  24. data/lib/peruby/lexer/number.rb +32 -0
  25. data/lib/peruby/lexer/quote_like.rb +72 -0
  26. data/lib/peruby/lexer/source_scanner.rb +104 -0
  27. data/lib/peruby/lexer/state.rb +46 -0
  28. data/lib/peruby/lexer/structure_scanner.rb +149 -0
  29. data/lib/peruby/lexer/term_scanner.rb +301 -0
  30. data/lib/peruby/lexer/token.rb +16 -0
  31. data/lib/peruby/lexer.rb +123 -0
  32. data/lib/peruby/node.rb +88 -0
  33. data/lib/peruby/op/assign.rb +128 -0
  34. data/lib/peruby/op/builtin.rb +903 -0
  35. data/lib/peruby/op/call.rb +378 -0
  36. data/lib/peruby/op/control.rb +256 -0
  37. data/lib/peruby/op/element.rb +136 -0
  38. data/lib/peruby/op/expression.rb +342 -0
  39. data/lib/peruby/op/io.rb +113 -0
  40. data/lib/peruby/op/list.rb +102 -0
  41. data/lib/peruby/op/literal.rb +84 -0
  42. data/lib/peruby/op/loop.rb +158 -0
  43. data/lib/peruby/op/regexp.rb +288 -0
  44. data/lib/peruby/op/variable.rb +534 -0
  45. data/lib/peruby/op.rb +47 -0
  46. data/lib/peruby/parser/grammar.rb +5797 -0
  47. data/lib/peruby/parser/grammar.y +576 -0
  48. data/lib/peruby/parser.rb +14 -0
  49. data/lib/peruby/runtime/code.rb +21 -0
  50. data/lib/peruby/runtime/conv.rb +140 -0
  51. data/lib/peruby/runtime/directory_handle.rb +18 -0
  52. data/lib/peruby/runtime/env.rb +129 -0
  53. data/lib/peruby/runtime/glob.rb +24 -0
  54. data/lib/peruby/runtime/interpolation.rb +223 -0
  55. data/lib/peruby/runtime/io_handle.rb +37 -0
  56. data/lib/peruby/runtime/local_stack.rb +90 -0
  57. data/lib/peruby/runtime/match_state.rb +62 -0
  58. data/lib/peruby/runtime/module_loader.rb +133 -0
  59. data/lib/peruby/runtime/mro.rb +94 -0
  60. data/lib/peruby/runtime/perl_array.rb +81 -0
  61. data/lib/peruby/runtime/perl_hash.rb +57 -0
  62. data/lib/peruby/runtime/ref.rb +43 -0
  63. data/lib/peruby/runtime/regexp_compiler.rb +75 -0
  64. data/lib/peruby/runtime/scalar.rb +27 -0
  65. data/lib/peruby/runtime/sprintf.rb +54 -0
  66. data/lib/peruby/runtime/stash.rb +50 -0
  67. data/lib/peruby/runtime/test_builder.rb +47 -0
  68. data/lib/peruby/runtime.rb +325 -0
  69. data/lib/peruby/validator.rb +236 -0
  70. data/lib/peruby/version.rb +5 -0
  71. data/lib/peruby.rb +31 -0
  72. data/t/00-basic.t +5 -0
  73. data/t/lib/MiniTest.pm +22 -0
  74. metadata +130 -0
@@ -0,0 +1,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