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,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
module Op
|
|
5
|
+
# Array/hash access over package aggregates or references, with autovivification.
|
|
6
|
+
class Element < Base
|
|
7
|
+
def initialize(container, key, kind, dereference, **options)
|
|
8
|
+
@container = container
|
|
9
|
+
@key = key
|
|
10
|
+
@kind = kind
|
|
11
|
+
@dereference = dereference
|
|
12
|
+
@slice = options.fetch(:slice, false)
|
|
13
|
+
@key_value = options.fetch(:key_value, false)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(env, context)
|
|
17
|
+
if @key_value && context == :list
|
|
18
|
+
container = aggregate(env)
|
|
19
|
+
return keys(env).flat_map { |key| [Scalar.new(Conv.to_str(key)), slot(container, key)] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
cells = lvalue(env, @slice ? :list : :scalar)
|
|
23
|
+
return cells if context == :list && cells.is_a?(Array)
|
|
24
|
+
|
|
25
|
+
cell = cells.is_a?(Array) ? cells.last : cells
|
|
26
|
+
context == :list ? [cell] : cell&.get
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_callable
|
|
30
|
+
return super if @slice || @key_value
|
|
31
|
+
|
|
32
|
+
key = @key.to_callable
|
|
33
|
+
lvalue = lvalue_callable
|
|
34
|
+
lambda do |env, context|
|
|
35
|
+
cell = lvalue.call(env, key.call(env, :scalar))
|
|
36
|
+
context == :list ? [cell] : cell&.value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def scalar_callable
|
|
41
|
+
return unless !@slice && !@key_value
|
|
42
|
+
|
|
43
|
+
key = @key.scalar_callable if @key.respond_to?(:scalar_callable)
|
|
44
|
+
key ||= ->(env) { @key.run(env, :scalar) }
|
|
45
|
+
lvalue = lvalue_callable
|
|
46
|
+
->(env) { lvalue.call(env, key.call(env))&.value }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def lvalue_callable
|
|
50
|
+
return ->(env, _key = nil) { lvalue(env, :list) } if @slice
|
|
51
|
+
|
|
52
|
+
container = aggregate_callable
|
|
53
|
+
kind = @kind
|
|
54
|
+
lambda do |env, supplied_key = nil|
|
|
55
|
+
key = supplied_key || @key.run(env, :scalar)
|
|
56
|
+
key = key.value if key.respond_to?(:value)
|
|
57
|
+
aggregate = container.call(env)
|
|
58
|
+
kind == :array ? aggregate.slot(Conv.to_num(key).to_i) : aggregate.slot(Conv.to_str(key))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def lvalue(env, context)
|
|
63
|
+
container = aggregate(env)
|
|
64
|
+
unless @slice
|
|
65
|
+
key = @key.run(env, :scalar)
|
|
66
|
+
cell = slot(container, key.respond_to?(:get) ? key.get : key)
|
|
67
|
+
return context == :list ? [cell] : cell
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
keys = @key.run(env, @slice ? :list : :scalar)
|
|
71
|
+
keys = Array(keys).map { |key| key.respond_to?(:get) ? key.get : key }
|
|
72
|
+
cells = keys.map { |key| slot(container, key) }
|
|
73
|
+
context == :list ? cells : cells.last
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def exists?(env)
|
|
77
|
+
container = aggregate(env)
|
|
78
|
+
keys(env).all? { |key| container.exists?(key) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def delete(env)
|
|
82
|
+
container = aggregate(env)
|
|
83
|
+
keys(env).map { |key| container.delete(key) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def localize(env, stack)
|
|
87
|
+
raise PerlError, "Can't localize slice" if @slice
|
|
88
|
+
|
|
89
|
+
container = aggregate(env)
|
|
90
|
+
key = @key.run(env, :scalar)
|
|
91
|
+
key = key.get if key.respond_to?(:get)
|
|
92
|
+
if @kind == :array
|
|
93
|
+
stack.localize_array(container, Conv.to_num(key).to_i)
|
|
94
|
+
else
|
|
95
|
+
stack.localize_hash(container, Conv.to_str(key))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def aggregate_callable
|
|
102
|
+
return ->(env) { aggregate(env) } unless @container.respond_to?(:aggregate_callable)
|
|
103
|
+
|
|
104
|
+
@container.aggregate_callable(@kind, dereference: @dereference)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def aggregate(env)
|
|
108
|
+
return @container.aggregate(env, @kind, dereference: @dereference) if @container.respond_to?(:aggregate)
|
|
109
|
+
|
|
110
|
+
dereference(@container.lvalue(env, :scalar))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def keys(env)
|
|
114
|
+
values = @key.run(env, @slice ? :list : :scalar)
|
|
115
|
+
Array(values).map { |key| key.respond_to?(:get) ? key.get : key }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def dereference(cell)
|
|
119
|
+
value = cell.get
|
|
120
|
+
if value.nil?
|
|
121
|
+
target = @kind == :array ? PerlArray.new : PerlHash.new
|
|
122
|
+
value = Ref.new(target)
|
|
123
|
+
cell.set(value)
|
|
124
|
+
end
|
|
125
|
+
expected = @kind == :array ? PerlArray : PerlHash
|
|
126
|
+
raise PerlError, "Not a #{@kind.to_s.upcase} reference" unless value.is_a?(Ref) && value.target.is_a?(expected)
|
|
127
|
+
|
|
128
|
+
value.target
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def slot(container, key)
|
|
132
|
+
@kind == :array ? container.slot(Conv.to_num(key).to_i) : container.slot(Conv.to_str(key))
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
module Op
|
|
5
|
+
# Unary scalar expression.
|
|
6
|
+
class Unary < Base
|
|
7
|
+
def initialize(operator, expression)
|
|
8
|
+
@operator = operator
|
|
9
|
+
@expression = expression
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run(env, context)
|
|
13
|
+
return reference(env, context) if @operator == :reference
|
|
14
|
+
|
|
15
|
+
value = @expression.run(env, :scalar)
|
|
16
|
+
if @operator == :negate && value.is_a?(Ref)
|
|
17
|
+
found, overloaded = env.runtime.overloaded(value, 'neg')
|
|
18
|
+
return context == :list ? [Scalar.new(overloaded)] : overloaded if found
|
|
19
|
+
end
|
|
20
|
+
operations = { negate: -> { -Conv.to_num(value) }, positive: -> { Conv.to_num(value) },
|
|
21
|
+
not: -> { Conv.truthy?(value) ? '' : 1 }, bit_not: -> { Conv.bit_not(value) } }
|
|
22
|
+
result = operations.fetch(@operator).call
|
|
23
|
+
context == :list ? [Scalar.new(result)] : result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reference(env, context)
|
|
27
|
+
return @expression.references(env, context) if @expression.respond_to?(:references)
|
|
28
|
+
|
|
29
|
+
target = begin
|
|
30
|
+
@expression.lvalue(env, :scalar)
|
|
31
|
+
rescue PerlError
|
|
32
|
+
Scalar.new(@expression.run(env, :scalar))
|
|
33
|
+
end
|
|
34
|
+
value = Ref.new(target)
|
|
35
|
+
context == :list ? [Scalar.new(value)] : value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Parenthesized expression, retained for Perl's \(LIST) reference semantics.
|
|
40
|
+
class Group < Base
|
|
41
|
+
def initialize(expression)
|
|
42
|
+
@expression = expression
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run(env, context) = @expression.run(env, context)
|
|
46
|
+
def lvalue(env, context) = @expression.lvalue(env, context)
|
|
47
|
+
def argument_cells(env) = @expression.argument_cells(env)
|
|
48
|
+
def aggregate(env, kind) = @expression.aggregate(env, kind)
|
|
49
|
+
|
|
50
|
+
def references(env, context)
|
|
51
|
+
values = @expression.argument_cells(env).map { |cell| Scalar.new(Ref.new(cell)) }
|
|
52
|
+
context == :list ? values : values.last&.get
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Post-increment/decrement preserving the old expression value.
|
|
57
|
+
class Postfix < Base
|
|
58
|
+
def initialize(operator, expression)
|
|
59
|
+
@operator = operator
|
|
60
|
+
@expression = expression
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def run(env, context)
|
|
64
|
+
cell = @expression.lvalue(env, :scalar)
|
|
65
|
+
old = cell.get
|
|
66
|
+
value = @operator == :increment ? Conv.increment(old) : Conv.to_num(old) - 1
|
|
67
|
+
cell.set(value)
|
|
68
|
+
context == :list ? [Scalar.new(old)] : old
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Pre-increment/decrement returning the updated value.
|
|
73
|
+
class Prefix < Base
|
|
74
|
+
def initialize(operator, expression)
|
|
75
|
+
@operator = operator
|
|
76
|
+
@expression = expression
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run(env, context)
|
|
80
|
+
cell = @expression.lvalue(env, :scalar)
|
|
81
|
+
value = @operator == :increment ? Conv.increment(cell.get) : Conv.to_num(cell.get) - 1
|
|
82
|
+
cell.set(value)
|
|
83
|
+
context == :list ? [cell] : value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Arithmetic, comparison, and short-circuit binary expression.
|
|
88
|
+
class Binary < Base
|
|
89
|
+
OVERLOAD_OPERATORS = {
|
|
90
|
+
:+ => '+', :- => '-', :* => '*', divide: '/', modulo: '%', concat: '.',
|
|
91
|
+
num_eq: '==', num_ne: '==', num_lt: '<=>', num_gt: '<=>', num_le: '<=>', num_ge: '<=>', num_cmp: '<=>',
|
|
92
|
+
str_eq: 'eq', str_ne: 'eq', str_lt: 'cmp', str_gt: 'cmp', str_le: 'cmp', str_ge: 'cmp', str_cmp: 'cmp'
|
|
93
|
+
}.freeze
|
|
94
|
+
NO_OVERLOAD = Object.new.freeze
|
|
95
|
+
OPERATIONS = {
|
|
96
|
+
:+ => ->(left, right) { Conv.norm_int(Conv.to_num(left) + Conv.to_num(right)) },
|
|
97
|
+
:- => ->(left, right) { Conv.norm_int(Conv.to_num(left) - Conv.to_num(right)) },
|
|
98
|
+
:* => ->(left, right) { Conv.norm_int(Conv.to_num(left) * Conv.to_num(right)) },
|
|
99
|
+
divide: ->(left, right) { Conv.divide(left, right) },
|
|
100
|
+
modulo: ->(left, right) { Conv.modulo(left, right) },
|
|
101
|
+
pow: ->(left, right) { Conv.norm_int(Conv.to_num(left)**Conv.to_num(right)) },
|
|
102
|
+
concat: ->(left, right) { Conv.to_str(left) + Conv.to_str(right) },
|
|
103
|
+
repeat: ->(left, right) { Conv.repeat(left, right) },
|
|
104
|
+
left_shift: ->(left, right) { Conv.to_num(left).to_i << Conv.to_num(right).to_i },
|
|
105
|
+
right_shift: ->(left, right) { Conv.to_num(left).to_i >> Conv.to_num(right).to_i },
|
|
106
|
+
bit_and: ->(left, right) { Conv.bitwise(:&, left, right) },
|
|
107
|
+
bit_or: ->(left, right) { Conv.bitwise(:|, left, right) },
|
|
108
|
+
bit_xor: ->(left, right) { Conv.bitwise(:^, left, right) }
|
|
109
|
+
}.freeze
|
|
110
|
+
|
|
111
|
+
def initialize(operator, left, right)
|
|
112
|
+
@operator = operator
|
|
113
|
+
@left = left
|
|
114
|
+
@right = right
|
|
115
|
+
@flip_flops = ObjectSpace::WeakMap.new if range?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def run(env, context)
|
|
119
|
+
return run_range(env, context) if range?
|
|
120
|
+
|
|
121
|
+
result = if %i[and or defined_or].include?(@operator)
|
|
122
|
+
short_circuit(env)
|
|
123
|
+
elsif @operator == :xor
|
|
124
|
+
Conv.truthy?(@left.run(env, :scalar)) ^ Conv.truthy?(@right.run(env, :scalar)) ? 1 : ''
|
|
125
|
+
else
|
|
126
|
+
calculate(@left.run(env, :scalar), @right.run(env, :scalar), env)
|
|
127
|
+
end
|
|
128
|
+
context == :list ? Array(result).map { |value| Scalar.new(value) } : result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def to_callable
|
|
132
|
+
return super if range?
|
|
133
|
+
|
|
134
|
+
scalar = scalar_callable
|
|
135
|
+
lambda do |env, context|
|
|
136
|
+
result = scalar.call(env)
|
|
137
|
+
context == :list ? Array(result).map { |value| Scalar.new(value) } : result
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def scalar_callable
|
|
142
|
+
return ->(env) { scalar_range(env) } if range?
|
|
143
|
+
|
|
144
|
+
left = child_scalar(@left)
|
|
145
|
+
right = child_scalar(@right)
|
|
146
|
+
return ->(env) { compiled_scalar_short_circuit(env, left, right) } if %i[and or defined_or].include?(@operator)
|
|
147
|
+
|
|
148
|
+
->(env) { compiled_calculate(left.call(env), right.call(env), env) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def child_scalar(operation)
|
|
154
|
+
callable = operation.scalar_callable if operation.respond_to?(:scalar_callable)
|
|
155
|
+
callable || ->(env) { operation.run(env, :scalar) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def compiled_scalar_short_circuit(env, left_operation, right_operation)
|
|
159
|
+
left = left_operation.call(env)
|
|
160
|
+
return Conv.truthy?(left) ? right_operation.call(env) : left if @operator == :and
|
|
161
|
+
return Conv.truthy?(left) ? left : right_operation.call(env) if @operator == :or
|
|
162
|
+
|
|
163
|
+
left.nil? ? right_operation.call(env) : left
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def compiled_calculate(left, right, env)
|
|
167
|
+
return calculate(left, right, env) if env.runtime.overloads?
|
|
168
|
+
|
|
169
|
+
if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
170
|
+
return Conv.norm_int(left + right) if @operator == :+
|
|
171
|
+
return Conv.norm_int(left - right) if @operator == :-
|
|
172
|
+
return Conv.norm_int(left * right) if @operator == :*
|
|
173
|
+
return Conv.divide(left, right) if @operator == :divide
|
|
174
|
+
return Conv.modulo(left, right) if @operator == :modulo
|
|
175
|
+
return Conv.norm_int(left**right) if @operator == :pow
|
|
176
|
+
return compiled_numeric_comparison(left, right) if @operator.to_s.start_with?('num_')
|
|
177
|
+
end
|
|
178
|
+
calculate(left, right, env)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def compiled_numeric_comparison(left, right)
|
|
182
|
+
comparison = left <=> right
|
|
183
|
+
case @operator
|
|
184
|
+
when :num_eq then comparison.zero? ? 1 : ''
|
|
185
|
+
when :num_ne then comparison.zero? ? '' : 1
|
|
186
|
+
when :num_lt then comparison.negative? ? 1 : ''
|
|
187
|
+
when :num_gt then comparison.positive? ? 1 : ''
|
|
188
|
+
when :num_le then comparison <= 0 ? 1 : ''
|
|
189
|
+
when :num_ge then comparison >= 0 ? 1 : ''
|
|
190
|
+
else comparison
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def short_circuit(env)
|
|
195
|
+
left = @left.run(env, :scalar)
|
|
196
|
+
return Conv.truthy?(left) ? @right.run(env, :scalar) : left if @operator == :and
|
|
197
|
+
return Conv.truthy?(left) ? left : @right.run(env, :scalar) if @operator == :or
|
|
198
|
+
|
|
199
|
+
left.nil? ? @right.run(env, :scalar) : left
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def compiled_short_circuit(env, left_operation, right_operation)
|
|
203
|
+
left = left_operation.call(env, :scalar)
|
|
204
|
+
return Conv.truthy?(left) ? right_operation.call(env, :scalar) : left if @operator == :and
|
|
205
|
+
return Conv.truthy?(left) ? left : right_operation.call(env, :scalar) if @operator == :or
|
|
206
|
+
|
|
207
|
+
left.nil? ? right_operation.call(env, :scalar) : left
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def calculate(left, right, env = nil)
|
|
211
|
+
overloaded = overload(left, right, env)
|
|
212
|
+
return overloaded unless overloaded.equal?(NO_OVERLOAD)
|
|
213
|
+
|
|
214
|
+
if OPERATIONS.key?(@operator)
|
|
215
|
+
operation = OPERATIONS.fetch(@operator)
|
|
216
|
+
warnable = !left.is_a?(Numeric) || !right.is_a?(Numeric)
|
|
217
|
+
if numeric_operator? && warnable && env&.runtime&.warnings_active?
|
|
218
|
+
return operation.call(env.runtime.number(left), env.runtime.number(right))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
return operation.call(left, right)
|
|
222
|
+
end
|
|
223
|
+
return compare(left, right, env) if @operator.to_s.start_with?('num_', 'str_')
|
|
224
|
+
return Conv.truthy?(left) ^ Conv.truthy?(right) ? 1 : '' if @operator == :xor
|
|
225
|
+
|
|
226
|
+
raise PerlError, "Unsupported binary operator #{@operator}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def overload(left, right, env)
|
|
230
|
+
operator = OVERLOAD_OPERATORS[@operator]
|
|
231
|
+
return NO_OVERLOAD unless env && operator && env.runtime.overloads?
|
|
232
|
+
|
|
233
|
+
found, value = env.runtime.overloaded(left, operator, right)
|
|
234
|
+
found, value = env.runtime.overloaded(right, operator, left, swapped: true) unless found
|
|
235
|
+
return NO_OVERLOAD unless found
|
|
236
|
+
|
|
237
|
+
return Conv.truthy?(value) ? '' : 1 if %i[num_ne str_ne].include?(@operator)
|
|
238
|
+
|
|
239
|
+
if @operator.to_s.match?(/_(?:lt|gt|le|ge)\z/)
|
|
240
|
+
relation = @operator.to_s.split('_').last
|
|
241
|
+
comparison = Conv.to_num(value)
|
|
242
|
+
matches = { 'lt' => comparison.negative?, 'gt' => comparison.positive?,
|
|
243
|
+
'le' => comparison <= 0, 'ge' => comparison >= 0 }
|
|
244
|
+
return matches.fetch(relation) ? 1 : ''
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
value
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def compare(left, right, env)
|
|
251
|
+
string = @operator.to_s.start_with?('str_')
|
|
252
|
+
comparison = string ? Conv.to_str(left) <=> Conv.to_str(right) : number(left, env) <=> number(right, env)
|
|
253
|
+
relation = @operator.to_s.delete_prefix('num_').delete_prefix('str_')
|
|
254
|
+
relations = { eq: comparison.zero?, ne: !comparison.zero?, lt: comparison.negative?, gt: comparison.positive?,
|
|
255
|
+
le: comparison <= 0, ge: comparison >= 0, cmp: comparison }
|
|
256
|
+
perl_boolean(relations.fetch(relation.to_sym))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def numeric_operator?
|
|
260
|
+
!%i[concat repeat].include?(@operator)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def number(value, env)
|
|
264
|
+
env&.runtime&.warnings_active? ? env.runtime.number(value) : Conv.to_num(value)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def perl_boolean(value)
|
|
268
|
+
return 1 if value == true
|
|
269
|
+
return '' if value == false
|
|
270
|
+
|
|
271
|
+
value
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def range? = %i[range range_exclusive].include?(@operator)
|
|
275
|
+
|
|
276
|
+
def run_range(env, context)
|
|
277
|
+
result = if context == :list
|
|
278
|
+
range_values(@left.run(env, :scalar), @right.run(env, :scalar))
|
|
279
|
+
else
|
|
280
|
+
scalar_range(env)
|
|
281
|
+
end
|
|
282
|
+
context == :list ? result.map { |value| Scalar.new(value) } : result
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# perlop: in scalar context .. and ... are stateful flip-flops, not list constructors.
|
|
286
|
+
def scalar_range(env)
|
|
287
|
+
owner = env.state_owner || env.runtime
|
|
288
|
+
count = @flip_flops[owner]
|
|
289
|
+
unless count
|
|
290
|
+
return '' unless Conv.truthy?(@left.run(env, :scalar))
|
|
291
|
+
|
|
292
|
+
count = 1
|
|
293
|
+
return finish_flip_flop(owner, count) if @operator == :range && Conv.truthy?(@right.run(env, :scalar))
|
|
294
|
+
|
|
295
|
+
@flip_flops[owner] = count
|
|
296
|
+
return count
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
count += 1
|
|
300
|
+
return finish_flip_flop(owner, count) if Conv.truthy?(@right.run(env, :scalar))
|
|
301
|
+
|
|
302
|
+
@flip_flops[owner] = count
|
|
303
|
+
count
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def finish_flip_flop(owner, count)
|
|
307
|
+
@flip_flops[owner] = nil
|
|
308
|
+
"#{count}E0"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def range_values(left, right)
|
|
312
|
+
return (Conv.to_str(left)..Conv.to_str(right)).to_a if left.is_a?(String) || right.is_a?(String)
|
|
313
|
+
|
|
314
|
+
(Conv.to_num(left).to_i..Conv.to_num(right).to_i).to_a
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Lazy conditional expression.
|
|
319
|
+
class Ternary < Base
|
|
320
|
+
def initialize(condition, true_expression, false_expression)
|
|
321
|
+
@condition = condition
|
|
322
|
+
@true_expression = true_expression
|
|
323
|
+
@false_expression = false_expression
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def run(env, context)
|
|
327
|
+
branch = Conv.truthy?(@condition.run(env, :scalar)) ? @true_expression : @false_expression
|
|
328
|
+
branch.run(env, context)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def to_callable
|
|
332
|
+
condition = @condition.to_callable
|
|
333
|
+
true_expression = @true_expression.to_callable
|
|
334
|
+
false_expression = @false_expression.to_callable
|
|
335
|
+
lambda do |env, context|
|
|
336
|
+
branch = Conv.truthy?(condition.call(env, :scalar)) ? true_expression : false_expression
|
|
337
|
+
branch.call(env, context)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
data/lib/peruby/op/io.rb
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
module Op
|
|
5
|
+
# Print operation with Perl output separators.
|
|
6
|
+
class Print < Base
|
|
7
|
+
def initialize(handle, arguments)
|
|
8
|
+
@handle = handle
|
|
9
|
+
@arguments = arguments
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run(env, context)
|
|
13
|
+
values = @arguments ? @arguments.run(env, :list) : [env.fetch(:scalar, '_')]
|
|
14
|
+
values.each { |cell| env.runtime.warning('uninitialized', 'Use of uninitialized value') if cell.get.nil? }
|
|
15
|
+
separator = Conv.to_str(env.fetch(:scalar, ',').get)
|
|
16
|
+
ending = Conv.to_str(env.fetch(:scalar, '\\').get)
|
|
17
|
+
env.runtime.output(@handle, env).print(values.map { |cell| Conv.to_str(cell.get) }.join(separator), ending)
|
|
18
|
+
context == :list ? [Scalar.new(1)] : 1
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Formatted output using the Perl sprintf compatibility layer.
|
|
23
|
+
class Printf < Base
|
|
24
|
+
def initialize(handle, arguments)
|
|
25
|
+
@handle = handle
|
|
26
|
+
@arguments = arguments
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run(env, context)
|
|
30
|
+
values = @arguments&.run(env, :list) || []
|
|
31
|
+
format = Conv.to_str(values.shift&.get)
|
|
32
|
+
env.runtime.output(@handle, env).print(Sprintf.format(format, values.map(&:get)))
|
|
33
|
+
context == :list ? [Scalar.new(1)] : 1
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# print with an implicit trailing newline.
|
|
38
|
+
class Say < Base
|
|
39
|
+
def initialize(handle, arguments)
|
|
40
|
+
@handle = handle
|
|
41
|
+
@arguments = arguments
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run(env, context)
|
|
45
|
+
values = @arguments ? @arguments.run(env, :list) : [env.fetch(:scalar, '_')]
|
|
46
|
+
env.runtime.output(@handle, env).puts(values.map { |cell| Conv.to_str(cell.get) }.join)
|
|
47
|
+
context == :list ? [Scalar.new(1)] : 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Diamond/readline operation in scalar or list context.
|
|
52
|
+
class Readline < Base
|
|
53
|
+
def initialize(handle)
|
|
54
|
+
@handle = handle
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run(env, context)
|
|
58
|
+
separator = env.fetch(:scalar, '/').get
|
|
59
|
+
if context == :list
|
|
60
|
+
values = []
|
|
61
|
+
loop do
|
|
62
|
+
record = read(env, separator)
|
|
63
|
+
break unless record
|
|
64
|
+
|
|
65
|
+
values << Scalar.new(record)
|
|
66
|
+
end
|
|
67
|
+
return values
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
read(env, separator)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def read(env, separator)
|
|
76
|
+
return env.runtime.read_argv(separator) if @handle.empty?
|
|
77
|
+
|
|
78
|
+
handle = resolve(env)
|
|
79
|
+
value = handle.read_record(separator)
|
|
80
|
+
env.runtime.note_input(handle) if value
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve(env)
|
|
85
|
+
return env.runtime.stash.glob('STDIN').io if @handle.nil? || @handle.empty?
|
|
86
|
+
|
|
87
|
+
if @handle.start_with?('$')
|
|
88
|
+
value = env.fetch(:scalar, @handle.delete_prefix('$')).get
|
|
89
|
+
return value if value.is_a?(IOHandle)
|
|
90
|
+
return value.target.io if value.is_a?(Ref) && value.target.is_a?(Glob)
|
|
91
|
+
end
|
|
92
|
+
lexical = env.fetch(:scalar, @handle).get
|
|
93
|
+
return lexical if lexical.is_a?(IOHandle)
|
|
94
|
+
|
|
95
|
+
env.runtime.stash.glob(@handle).io || raise(PerlError, "readline() on closed filehandle #{@handle}")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Perl's readline-in-while shorthand assigns $_ and tests definedness.
|
|
100
|
+
class ReadlineCondition < Base
|
|
101
|
+
def initialize(readline)
|
|
102
|
+
@readline = readline
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def run(env, context)
|
|
106
|
+
value = @readline.run(env, :scalar)
|
|
107
|
+
env.fetch(:scalar, '_').set(value)
|
|
108
|
+
result = value.nil? ? '' : 1
|
|
109
|
+
context == :list ? [Scalar.new(result)] : result
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Peruby
|
|
4
|
+
module Op
|
|
5
|
+
# Perl comma list with context-dependent evaluation.
|
|
6
|
+
class List < Base
|
|
7
|
+
def initialize(items)
|
|
8
|
+
@items = items
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(env, context)
|
|
12
|
+
return @items.flat_map { |item| item.run(env, :list) } if context == :list
|
|
13
|
+
return nil if @items.empty?
|
|
14
|
+
|
|
15
|
+
@items[0...-1].each { |item| item.run(env, :void) }
|
|
16
|
+
@items.last.run(env, context)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def argument_cells(env, prototype = nil)
|
|
20
|
+
return prototype_cells(env, prototype) unless prototype.nil?
|
|
21
|
+
return @items.first.argument_cells(env) if @items.length == 1
|
|
22
|
+
|
|
23
|
+
@items.flat_map { |item| item.argument_cells(env) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def argument_callable
|
|
27
|
+
return ->(_env, _prototype = nil) { [] } if @items.empty?
|
|
28
|
+
|
|
29
|
+
operations = @items.map(&:to_callable)
|
|
30
|
+
lambda do |env, prototype = nil|
|
|
31
|
+
next prototype_cells(env, prototype) if prototype
|
|
32
|
+
|
|
33
|
+
operations.flat_map { |operation| operation.call(env, :list) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def empty? = @items.empty?
|
|
38
|
+
|
|
39
|
+
def lvalue(env, _context)
|
|
40
|
+
@items.flat_map do |item|
|
|
41
|
+
value = item.lvalue(env, :scalar)
|
|
42
|
+
value.is_a?(Array) ? value : [value]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def prototype_cells(env, prototype)
|
|
49
|
+
specs = prototype.delete(';').scan(/\\?[$@%&*+_]/)
|
|
50
|
+
cells = []
|
|
51
|
+
@items.each_with_index do |item, index|
|
|
52
|
+
spec = specs[index]
|
|
53
|
+
if %w[@ %].include?(spec)
|
|
54
|
+
cells.concat(@items[index..].flat_map { |rest| rest.argument_cells(env) })
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
cells << prototype_cell(env, item, spec)
|
|
59
|
+
end
|
|
60
|
+
cells
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def prototype_cell(env, item, spec)
|
|
64
|
+
return Scalar.new(Ref.new(item.aggregate(env, spec == '\\@' ? :array : :hash))) if %w[\@ \%].include?(spec)
|
|
65
|
+
return item.argument_cells(env).first if %w[& *].include?(spec)
|
|
66
|
+
|
|
67
|
+
cell = item.lvalue(env, :scalar)
|
|
68
|
+
cell.is_a?(Scalar) ? cell : Scalar.new(item.run(env, :scalar))
|
|
69
|
+
rescue PerlError
|
|
70
|
+
Scalar.new(item.run(env, :scalar))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Anonymous array reference.
|
|
75
|
+
class ArrayLiteral < Base
|
|
76
|
+
def initialize(items)
|
|
77
|
+
@items = items
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run(env, context)
|
|
81
|
+
cells = @items.flat_map { |item| item.run(env, :list) }.map(&:copy)
|
|
82
|
+
value = Ref.new(PerlArray.new(cells))
|
|
83
|
+
context == :list ? [Scalar.new(value)] : value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Anonymous hash reference.
|
|
88
|
+
class HashLiteral < Base
|
|
89
|
+
def initialize(items)
|
|
90
|
+
@items = items
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def run(env, context)
|
|
94
|
+
values = @items.flat_map { |item| item.run(env, :list) }
|
|
95
|
+
hash = PerlHash.new
|
|
96
|
+
values.each_slice(2) { |key, value| hash.slot(Conv.to_str(key&.get)).set(value&.get) }
|
|
97
|
+
ref = Ref.new(hash)
|
|
98
|
+
context == :list ? [Scalar.new(ref)] : ref
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|