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,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
|