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,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Perl scalar coercion rules used by all operators and built-ins.
5
+ module Conv
6
+ IV_MAX = (2**63) - 1
7
+ IV_MIN = -(2**63)
8
+ NUM_RE = /\A\s*([+-]?(?:(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|Inf(?:inity)?|NaN))/i
9
+ MAGICAL_INCREMENT = /\A[a-zA-Z]*[0-9]*\z/
10
+
11
+ module_function
12
+
13
+ def truthy?(value)
14
+ if value.is_a?(Ref) && value.runtime
15
+ found, overloaded = value.runtime.overloaded(value, 'bool')
16
+ return truthy?(overloaded) if found
17
+ end
18
+ case value
19
+ when nil then false
20
+ when String then !value.empty? && value != '0'
21
+ when Numeric then !value.zero?
22
+ when Dualvar then truthy?(value.str)
23
+ else true
24
+ end
25
+ end
26
+
27
+ def to_num(value)
28
+ case value
29
+ when Integer, Float then value
30
+ when Dualvar then value.num
31
+ when Ref
32
+ found, overloaded = value.runtime&.overloaded(value, '0+')
33
+ found ? to_num(overloaded) : value.target.object_id
34
+ when String then parse_num(value)
35
+ else 0
36
+ end
37
+ end
38
+
39
+ def to_str(value)
40
+ case value
41
+ when nil then ''
42
+ when Float then format_float(value)
43
+ when Dualvar then value.str
44
+ when Ref
45
+ found, overloaded = value.runtime&.overloaded(value, '""')
46
+ found ? to_str(overloaded) : ref_to_str(value)
47
+ when Glob then "*#{value.full_name}"
48
+ else value.to_s
49
+ end
50
+ end
51
+
52
+ def norm_int(number)
53
+ return number.to_f if number.is_a?(Integer) && !number.between?(IV_MIN, IV_MAX)
54
+
55
+ number
56
+ end
57
+
58
+ def increment(value)
59
+ string = to_str(value)
60
+ return string.succ if MAGICAL_INCREMENT.match?(string) && string.match?(/[a-zA-Z0-9]/)
61
+
62
+ norm_int(to_num(value) + 1)
63
+ end
64
+
65
+ # perlop: classic bitwise operators use byte strings only when both operands are strings.
66
+ def bitwise(operator, left, right)
67
+ unless left.is_a?(String) && right.is_a?(String)
68
+ return to_num(left).to_i.public_send(operator, to_num(right).to_i)
69
+ end
70
+
71
+ left_bytes = left.bytes
72
+ right_bytes = right.bytes
73
+ lengths = [left_bytes.length, right_bytes.length]
74
+ length = operator == :& ? lengths.min : lengths.max
75
+ Array.new(length) { |index| (left_bytes[index] || 0).public_send(operator, right_bytes[index] || 0) }.pack('C*')
76
+ end
77
+
78
+ def bit_not(value)
79
+ return ~to_num(value).to_i unless value.is_a?(String)
80
+
81
+ value.bytes.map { |byte| byte ^ 0xff }.pack('C*')
82
+ end
83
+
84
+ def divide(left, right)
85
+ divisor = to_num(right)
86
+ raise PerlError, 'Illegal division by zero' if divisor.zero?
87
+
88
+ to_num(left).fdiv(divisor)
89
+ end
90
+
91
+ def modulo(left, right)
92
+ divisor = to_num(right)
93
+ raise PerlError, 'Illegal modulus zero' if divisor.zero?
94
+
95
+ to_num(left) % divisor
96
+ end
97
+
98
+ def repeat(value, count)
99
+ to_str(value) * [to_num(count).to_i, 0].max
100
+ end
101
+
102
+ def parse_num(value)
103
+ match = NUM_RE.match(value)
104
+ return 0 unless match
105
+
106
+ number = match[1]
107
+ return Float::NAN if number.casecmp('nan').zero?
108
+ return number.start_with?('-') ? -Float::INFINITY : Float::INFINITY if number.match?(/inf/i)
109
+
110
+ number.match?(/[.e]/i) ? number.to_f : number.to_i
111
+ end
112
+ private_class_method :parse_num
113
+
114
+ def format_float(value)
115
+ return '0' if value.zero?
116
+ return 'NaN' if value.nan?
117
+ return value.negative? ? '-Inf' : 'Inf' if value.infinite?
118
+
119
+ format('%.15g', value)
120
+ end
121
+ private_class_method :format_float
122
+
123
+ def ref_to_str(ref)
124
+ return regexp_to_str(ref.target) if ref.target.is_a?(Regexp)
125
+
126
+ prefix = ref.blessed ? "#{ref.blessed}=" : ''
127
+ "#{prefix}#{ref.kind}(0x#{ref.target.object_id.to_s(16)})"
128
+ end
129
+ private_class_method :ref_to_str
130
+
131
+ def regexp_to_str(regexp)
132
+ flags = +''
133
+ flags << 'i' if regexp.options.anybits?(Regexp::IGNORECASE)
134
+ flags << 's' if regexp.options.anybits?(Regexp::MULTILINE)
135
+ flags << 'x' if regexp.options.anybits?(Regexp::EXTENDED)
136
+ "(?^#{flags}:#{regexp.source})"
137
+ end
138
+ private_class_method :regexp_to_str
139
+ end
140
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Stateful Perl directory handle backed by Ruby's Dir.
5
+ class DirectoryHandle
6
+ def initialize(path)
7
+ @directory = Dir.open(path)
8
+ end
9
+
10
+ def read = @directory.read
11
+ def rewind = @directory.rewind && 1
12
+
13
+ def close
14
+ @directory.close
15
+ 1
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Runtime lexical environment and package fallback.
5
+ class Env
6
+ attr_reader :runtime, :want, :package, :generation, :state_owner
7
+
8
+ def initialize(runtime, scopes: nil, want: :void, package: 'main', state_owner: nil, strict: nil) # rubocop:disable Metrics/ParameterLists
9
+ @runtime = runtime
10
+ @scopes = scopes || [{}]
11
+ @want = want
12
+ @package = package
13
+ @state_owner = state_owner
14
+ @strict = strict || Set.new
15
+ @generation = 0
16
+ @lookup_cache = {}
17
+ end
18
+
19
+ def with_scope(&)
20
+ package = @package
21
+ strict = @strict.dup
22
+ @scopes << {}
23
+ @runtime.local_stack.within { @runtime.with_match_scope(&) }
24
+ ensure
25
+ changed_package = @package != package
26
+ @package = package
27
+ @strict = strict
28
+ invalidate if changed_package || !@scopes.pop.empty?
29
+ end
30
+
31
+ def with_package(package)
32
+ previous = @package
33
+ self.package = package
34
+ yield
35
+ ensure
36
+ self.package = previous
37
+ end
38
+
39
+ def package=(package)
40
+ @package = package
41
+ invalidate
42
+ end
43
+
44
+ def configure_strict(categories, disable: false)
45
+ categories = %w[refs subs vars] if categories.empty?
46
+ disable ? @strict.subtract(categories) : @strict.merge(categories)
47
+ end
48
+
49
+ def strict_refs? = @strict.include?('refs')
50
+ def strict_categories = @strict.dup
51
+
52
+ def declare(sigil, name)
53
+ invalidate
54
+ @scopes.last[[sigil, name]] = empty_value(sigil)
55
+ end
56
+
57
+ def bind(sigil, name, value)
58
+ invalidate
59
+ @scopes.last[[sigil, name]] = value
60
+ end
61
+
62
+ def fork(want: :void, package: @package, state_owner: nil)
63
+ self.class.new(@runtime, scopes: @scopes.dup << {}, want:, package:, state_owner:, strict: @strict.dup)
64
+ end
65
+
66
+ def call_frame(arguments, want:, package:, state_owner:)
67
+ arguments_scope = { [:array, '_'].freeze => PerlArray.new(arguments) }
68
+ self.class.new(@runtime, scopes: @scopes.dup << arguments_scope, want:, package:, state_owner:,
69
+ strict: @strict.dup)
70
+ end
71
+
72
+ def capture
73
+ self.class.new(@runtime, scopes: @scopes.map(&:dup), want: @want, package: @package, state_owner: @state_owner,
74
+ strict: @strict.dup)
75
+ end
76
+
77
+ def fetch(sigil, name)
78
+ fetch_key([sigil, name])
79
+ end
80
+
81
+ def fetch_key(key)
82
+ sigil, name = key
83
+ match_value = @runtime.match_state.fetch(sigil, name)
84
+ return match_value if match_value
85
+
86
+ cached = @lookup_cache[key]
87
+ return cached if cached
88
+
89
+ scope = @scopes.reverse_each.find { |candidate| candidate.key?(key) }
90
+ return @lookup_cache[key] = scope[key] if scope
91
+
92
+ @lookup_cache[key] = glob_value(sigil, name)
93
+ end
94
+
95
+ private
96
+
97
+ def invalidate
98
+ @generation += 1
99
+ @lookup_cache.clear
100
+ end
101
+
102
+ def empty_value(sigil)
103
+ case sigil
104
+ when :scalar then Scalar.new
105
+ when :array then PerlArray.new
106
+ when :hash then PerlHash.new
107
+ else raise ArgumentError, "unknown sigil: #{sigil}"
108
+ end
109
+ end
110
+
111
+ def glob_value(sigil, name)
112
+ name = "#{@package}::#{name}" if package_variable?(name)
113
+ glob = @runtime.stash.glob(name)
114
+ case sigil
115
+ when :scalar then glob.scalar
116
+ when :array then glob.array
117
+ when :hash then glob.hash
118
+ when :code then glob.code
119
+ when :glob then glob
120
+ else raise ArgumentError, "unknown sigil: #{sigil}"
121
+ end
122
+ end
123
+
124
+ def package_variable?(name)
125
+ !name.include?('::') && !name.include?("'") && !name.match?(/\A(?:\d+|\W|\^.)\z/) &&
126
+ !%w[_ ARGV ENV INC SIG].include?(name)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Five independent value slots associated with one Perl symbol name.
5
+ class Glob
6
+ SLOTS = %i[scalar array hash code io].freeze
7
+
8
+ attr_reader :full_name
9
+ attr_accessor(*SLOTS)
10
+
11
+ def initialize(full_name)
12
+ @full_name = full_name
13
+ @scalar = Scalar.new
14
+ @array = PerlArray.new
15
+ @hash = PerlHash.new
16
+ end
17
+
18
+ def replace(slot, value)
19
+ raise ArgumentError, "unknown glob slot: #{slot}" unless SLOTS.include?(slot)
20
+
21
+ public_send("#{slot}=", value)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Expands the escape and variable forms needed by interpolating strings.
5
+ module Interpolation
6
+ ESCAPED_DOLLAR = "\u0000DOLLAR\u0000"
7
+ ESCAPED_ARRAY = "\u0000ARRAY\u0000"
8
+ ESCAPED_BACKSLASH = "\u0000BACKSLASH\u0000"
9
+ NAME = /[A-Za-z_]\w*(?:(?:::|')[A-Za-z_]\w*)*/
10
+ ESCAPES = { 'n' => "\n", 't' => "\t", 'r' => "\r", 'f' => "\f", 'b' => "\b",
11
+ 'a' => "\a", 'e' => "\e", '\\' => '\\', '"' => '"', '$' => '$', '@' => '@' }.freeze
12
+
13
+ module_function
14
+
15
+ def expand(quote, env)
16
+ raw = quote.parts.first
17
+ return escapes(raw) unless quote.interpolate
18
+
19
+ text = interpolate_expressions(raw, env)
20
+ cases(escapes(text)).gsub(ESCAPED_DOLLAR, '$').gsub(ESCAPED_ARRAY, '@').gsub(ESCAPED_BACKSLASH, '\\')
21
+ end
22
+
23
+ def pattern(quote, env)
24
+ raw = quote.parts.first.gsub('\\$', ESCAPED_DOLLAR).gsub('\\@', ESCAPED_ARRAY)
25
+ text = raw.gsub(variable_pattern) do
26
+ sigil = Regexp.last_match(1) == '$' ? :scalar : :array
27
+ name = Regexp.last_match(2) || Regexp.last_match(3)
28
+ interpolate(env.fetch(sigil, name), sigil, env)
29
+ end
30
+ text.gsub(ESCAPED_DOLLAR, '\\$').gsub(ESCAPED_ARRAY, '\\@')
31
+ end
32
+
33
+ def variable_pattern
34
+ /([$@])(?:\{(#{NAME.source})\}|(#{NAME.source}|\d+|[&`'+]))/
35
+ end
36
+
37
+ def escapes(text)
38
+ text.gsub(/\\(?:x\{([0-9a-fA-F]+)\}|x([0-9a-fA-F]{1,2})|0([0-7]{0,2})|c(.)|N\{U\+([0-9a-fA-F]+)\}|(.))/) do
39
+ escape_match(Regexp.last_match)
40
+ end
41
+ end
42
+
43
+ def escape_match(match)
44
+ return match[1].to_i(16).chr(Encoding::UTF_8) if match[1]
45
+ return match[2].to_i(16).chr(Encoding::UTF_8) if match[2]
46
+ return match[3].to_i(8).chr if match[3]
47
+ return (match[4].ord & 31).chr if match[4]
48
+ return match[5].to_i(16).chr(Encoding::UTF_8) if match[5]
49
+
50
+ return "\\#{match[6]}" if %w[U L Q u l E].include?(match[6])
51
+
52
+ ESCAPES.fetch(match[6], match[6])
53
+ end
54
+
55
+ def interpolate(value, sigil, env)
56
+ return Conv.to_str(value.get) if sigil == :scalar
57
+
58
+ value.list.map { |cell| Conv.to_str(cell.get) }.join(Conv.to_str(env.fetch(:scalar, '"').get))
59
+ end
60
+
61
+ def interpolate_expressions(source, env)
62
+ output = +''
63
+ index = 0
64
+ while index < source.length
65
+ if source[index] == '\\'
66
+ output << source[index, 2]
67
+ index += 2
68
+ next
69
+ end
70
+ unless '$@'.include?(source[index])
71
+ output << source[index]
72
+ index += 1
73
+ next
74
+ end
75
+
76
+ length, value = interpolation_at(source, index, env)
77
+ unless length
78
+ output << source[index]
79
+ index += 1
80
+ next
81
+ end
82
+ output << protect(value)
83
+ index += length
84
+ end
85
+ output
86
+ end
87
+
88
+ # rubocop:disable-next Metrics/AbcSize
89
+ def interpolation_at(source, index, env)
90
+ marker = source[index]
91
+ return array_last(source, index, env) if marker == '$' && source[index + 1] == '#'
92
+ return scalar_dereference(source, index, env) if marker == '$' && source[index + 1] == '$'
93
+ return braced_interpolation(source, index, env) if source[index + 1] == '{'
94
+
95
+ name = source[(index + 1)..].match(/\A(#{NAME.source}|\d+|[&`'+])/)&.[](1)
96
+ return [nil, nil] unless name
97
+
98
+ finish = consume_access_chain(source, index + 1 + name.length)
99
+ expression = source[index...finish]
100
+ value = if finish == index + 1 + name.length
101
+ interpolate(env.fetch(marker == '$' ? :scalar : :array, name), marker == '$' ? :scalar : :array, env)
102
+ else
103
+ evaluate(expression, env, marker == '$' ? :scalar : :list)
104
+ end
105
+ [finish - index, stringify(value, marker, env)]
106
+ end
107
+
108
+ def braced_interpolation(source, index, env)
109
+ finish = balanced_finish(source, index + 1)
110
+ return [nil, nil] unless finish
111
+
112
+ body = source[(index + 2)...(finish - 1)].strip
113
+ marker = source[index]
114
+ value = if marker == '$' && body.match?(/\A#{NAME.source}\z/)
115
+ env.fetch(:scalar, body).get
116
+ else
117
+ dereferenced(evaluate(body, env, :scalar), marker)
118
+ end
119
+ [finish - index, stringify(value, marker, env)]
120
+ end
121
+
122
+ def array_last(source, index, env)
123
+ name = source[(index + 2)..].match(/\A(#{NAME.source})/)&.[](1)
124
+ return [nil, nil] unless name
125
+
126
+ [name.length + 2, (env.fetch(:array, name).size - 1).to_s]
127
+ end
128
+
129
+ def scalar_dereference(source, index, env)
130
+ name = source[(index + 2)..].match(/\A(#{NAME.source})/)&.[](1)
131
+ return [nil, nil] unless name
132
+
133
+ [name.length + 2, Conv.to_str(dereferenced(env.fetch(:scalar, name).get, '$'))]
134
+ end
135
+
136
+ def consume_access_chain(source, index)
137
+ loop do
138
+ start = if source[index, 2] == '->' && source[index + 2] && '[{'.include?(source[index + 2])
139
+ index + 2
140
+ elsif source[index] && '[{'.include?(source[index])
141
+ index
142
+ end
143
+ break unless start
144
+
145
+ index = balanced_finish(source, start) || index
146
+ end
147
+ index
148
+ end
149
+
150
+ def balanced_finish(source, start)
151
+ pairs = { '{' => '}', '[' => ']' }
152
+ close = pairs[source[start]]
153
+ return unless close
154
+
155
+ depth = 1
156
+ index = start + 1
157
+ while index < source.length
158
+ depth += 1 if source[index] == source[start]
159
+ depth -= 1 if source[index] == close
160
+ return index + 1 if depth.zero?
161
+
162
+ index += source[index] == '\\' ? 2 : 1
163
+ end
164
+ end
165
+
166
+ def evaluate(source, env, context)
167
+ tree = Parser.parse("#{source};", file: env.runtime.current_file, package: env.package)
168
+ Compiler.new.compile(tree).run(env, context)
169
+ end
170
+
171
+ def dereferenced(value, marker)
172
+ return value unless value.is_a?(Ref)
173
+ return value.target.get if marker == '$' && value.target.is_a?(Scalar)
174
+ return value.target.list if marker == '@' && value.target.is_a?(PerlArray)
175
+
176
+ value
177
+ end
178
+
179
+ def stringify(value, marker, env)
180
+ if marker == '@'
181
+ return Array(value).map { |item| Conv.to_str(item.respond_to?(:get) ? item.get : item) }
182
+ .join(Conv.to_str(env.fetch(:scalar, '"').get))
183
+ end
184
+
185
+ Conv.to_str(value.respond_to?(:get) ? value.get : value)
186
+ end
187
+
188
+ def protect(value)
189
+ value.gsub('\\', ESCAPED_BACKSLASH).gsub('$', ESCAPED_DOLLAR).gsub('@', ESCAPED_ARRAY)
190
+ end
191
+
192
+ def cases(text)
193
+ output = +''
194
+ mode = nil
195
+ quote = false
196
+ once = nil
197
+ index = 0
198
+ while index < text.length
199
+ control = text[index, 2]
200
+ if control.match?(/\A\\[ULQulE]\z/)
201
+ command = control[1]
202
+ mode = command if %w[U L].include?(command)
203
+ quote = true if command == 'Q'
204
+ once = command if %w[u l].include?(command)
205
+ if command == 'E'
206
+ mode = once = nil
207
+ quote = false
208
+ end
209
+ index += 2
210
+ next
211
+ end
212
+
213
+ character = text[index]
214
+ character = mode == 'U' ? character.upcase : character.downcase if mode
215
+ character = once == 'u' ? character.upcase : character.downcase if once
216
+ once = nil
217
+ output << (quote ? Regexp.escape(character) : character)
218
+ index += 1
219
+ end
220
+ output
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Perl filehandle state over a Ruby IO object.
5
+ class IOHandle
6
+ attr_accessor :io, :lineno, :eof_flag, :layers
7
+
8
+ def initialize(io, layers: [])
9
+ @io = io
10
+ @lineno = 0
11
+ @eof_flag = false
12
+ @layers = layers
13
+ end
14
+
15
+ def read_record(separator)
16
+ value = if separator.nil?
17
+ @io.read
18
+ elsif separator == ''
19
+ @io.gets("\n\n")
20
+ elsif separator.is_a?(Ref) && separator.target.is_a?(Scalar)
21
+ @io.read(Conv.to_num(separator.target.get).to_i)
22
+ else
23
+ @io.gets(Conv.to_str(separator))
24
+ end
25
+ @eof_flag = value.nil?
26
+ @lineno += 1 if value
27
+ value
28
+ end
29
+
30
+ def close
31
+ @io.close unless @io.closed?
32
+ 1
33
+ rescue IOError, SystemCallError
34
+ ''
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Peruby
4
+ # Restores dynamically localized values in reverse order.
5
+ class LocalStack
6
+ def initialize
7
+ @restorers = []
8
+ end
9
+
10
+ def mark
11
+ @restorers.size
12
+ end
13
+
14
+ def localize_glob(glob, slot, replacement = nil)
15
+ old = glob.public_send(slot)
16
+ @restorers << -> { glob.replace(slot, old) }
17
+ glob.replace(slot, replacement || default_for(slot))
18
+ end
19
+
20
+ def localize_hash(hash, key)
21
+ key = key.to_s
22
+ old = hash.cells[key]
23
+ @restorers << -> { hash.replace_cell(key, old) }
24
+ hash.replace_cell(key, Scalar.new)
25
+ end
26
+
27
+ def localize_array(array, index)
28
+ index += array.size if index.negative?
29
+ old = array.cells[index]
30
+ @restorers << -> { array.replace_cell(index, old) }
31
+ array.replace_cell(index, Scalar.new)
32
+ end
33
+
34
+ def localize_scalar(scalar)
35
+ old = scalar.get
36
+ @restorers << -> { scalar.set(old) }
37
+ scalar.set(nil)
38
+ end
39
+
40
+ def localize_array_value(array)
41
+ old = array.cells.dup
42
+ @restorers << -> { array.cells.replace(old) }
43
+ array.cells.clear
44
+ array
45
+ end
46
+
47
+ def localize_hash_value(hash)
48
+ old = hash.cells.dup
49
+ @restorers << -> { hash.cells.replace(old) }
50
+ hash.cells.clear
51
+ hash
52
+ end
53
+
54
+ def localize_entire_glob(glob) # rubocop:disable Metrics/AbcSize
55
+ scalar = glob.scalar
56
+ array = glob.array
57
+ hash = glob.hash
58
+ old = [scalar.get, array.cells.dup, hash.cells.dup, glob.code, glob.io]
59
+ @restorers << lambda do
60
+ scalar.set(old[0])
61
+ array.cells.replace(old[1])
62
+ hash.cells.replace(old[2])
63
+ glob.code = old[3]
64
+ glob.io = old[4]
65
+ end
66
+ scalar.set(nil)
67
+ array.cells.clear
68
+ hash.cells.clear
69
+ glob.code = glob.io = nil
70
+ glob
71
+ end
72
+
73
+ def unwind_to(mark)
74
+ @restorers.pop.call while @restorers.size > mark
75
+ end
76
+
77
+ def within
78
+ saved = mark
79
+ yield
80
+ ensure
81
+ unwind_to(saved)
82
+ end
83
+
84
+ private
85
+
86
+ def default_for(slot)
87
+ { scalar: Scalar.new, array: PerlArray.new, hash: PerlHash.new }[slot]
88
+ end
89
+ end
90
+ end