srsh 0.8.0 → 1.0.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 +4 -4
- data/LICENSE +12 -3
- data/README.md +446 -8
- data/bin/srsh +71 -0
- data/docs/assets/slut.txt +4 -0
- data/docs/assets/srsh-mark.svg +12 -0
- data/docs/css/style.css +696 -0
- data/docs/index.html +703 -0
- data/docs/js/app.js +203 -0
- data/examples/bridge.rsh +8 -0
- data/examples/calculator.rsh +253 -0
- data/examples/defer.rsh +14 -0
- data/examples/hot.rsh +14 -0
- data/examples/meta.rsh +20 -0
- data/examples/modules/text.rsh +6 -0
- data/examples/modules.rsh +6 -0
- data/examples/paste.rsh +15 -0
- data/examples/plugin.rb +8 -0
- data/examples/power.rsh +65 -0
- data/examples/tour.rsh +38 -0
- data/ext/srsh_native/extconf.rb +3 -0
- data/ext/srsh_native/srsh_native.c +48 -0
- data/language-docs/LANGUAGE.md +670 -0
- data/language-docs/MIGRATION.md +44 -0
- data/language-docs/SECURITY.md +44 -0
- data/lib/srsh/app.rb +261 -0
- data/lib/srsh/builtins.rb +492 -0
- data/lib/srsh/editor.rb +530 -0
- data/lib/srsh/errors.rb +23 -0
- data/lib/srsh/history.rb +74 -0
- data/lib/srsh/language/evaluator.rb +1175 -0
- data/lib/srsh/language/lexer.rb +316 -0
- data/lib/srsh/language/parser.rb +997 -0
- data/lib/srsh/language/token.rb +5 -0
- data/lib/srsh/language/values.rb +392 -0
- data/lib/srsh/paths.rb +29 -0
- data/lib/srsh/plugins.rb +59 -0
- data/lib/srsh/process_identity.rb +38 -0
- data/lib/srsh/security.rb +38 -0
- data/lib/srsh/shell/executor.rb +1182 -0
- data/lib/srsh/shell/job.rb +101 -0
- data/lib/srsh/shell/lexer.rb +114 -0
- data/lib/srsh/shell/terminal.rb +26 -0
- data/lib/srsh/state.rb +136 -0
- data/lib/srsh/theme.rb +108 -0
- data/lib/srsh/version.rb +3 -0
- data/lib/srsh.rb +11 -5
- metadata +61 -14
- data/exe/srsh +0 -6
- data/lib/srsh/runner.rb +0 -2416
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'etc'
|
|
4
|
+
require 'thread'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'shellwords'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require_relative '../errors'
|
|
9
|
+
require_relative 'values'
|
|
10
|
+
|
|
11
|
+
module Srsh
|
|
12
|
+
module Language
|
|
13
|
+
LambdaValue = Data.define(:params, :body, :captured)
|
|
14
|
+
CodeValue = Data.define(:source, :nodes)
|
|
15
|
+
|
|
16
|
+
class Evaluator
|
|
17
|
+
CACHE_LIMIT = 4096
|
|
18
|
+
MAX_LAMBDA_DEPTH = 128
|
|
19
|
+
MAX_FILE_READ = 16 * 1024 * 1024
|
|
20
|
+
|
|
21
|
+
def initialize(state, executor = nil)
|
|
22
|
+
@state = state
|
|
23
|
+
@executor = executor
|
|
24
|
+
@ast_cache = {}
|
|
25
|
+
@lambda_depth = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_writer :executor
|
|
29
|
+
|
|
30
|
+
def parse_eval(source, line: 1)
|
|
31
|
+
key = [source, line]
|
|
32
|
+
ast = @ast_cache[key]
|
|
33
|
+
unless ast
|
|
34
|
+
@ast_cache.clear if @ast_cache.length >= CACHE_LIMIT
|
|
35
|
+
ast = @ast_cache[key] = ExprParser.new(source, line: line).parse
|
|
36
|
+
end
|
|
37
|
+
eval_ast(ast)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def truthy?(value) = !(value.nil? || value == false)
|
|
41
|
+
|
|
42
|
+
def eval_ast(ast)
|
|
43
|
+
kind = ast[0]
|
|
44
|
+
case kind
|
|
45
|
+
when :literal then ast[1]
|
|
46
|
+
when :template
|
|
47
|
+
ast[1].map { |part| part[0] == :text ? part[1] : stringify(eval_ast(part[1])) }.join
|
|
48
|
+
when :capture
|
|
49
|
+
raise RuntimeError, 'command capture unavailable here' unless @executor
|
|
50
|
+
@executor.capture(ast[1])
|
|
51
|
+
when :lambda
|
|
52
|
+
LambdaValue.new(ast[1].freeze, ast[2], @state.locals_snapshot.freeze)
|
|
53
|
+
when :spawn
|
|
54
|
+
spawn_task(eval_ast(ast[1]), [])
|
|
55
|
+
when :local
|
|
56
|
+
name = ast[1]
|
|
57
|
+
return @state.local_get(name) if @state.local_defined?(name)
|
|
58
|
+
return FunctionRef.new(name, @state.locals_snapshot.freeze) if @executor&.function?(name)
|
|
59
|
+
return PrototypeRef.new(name) if @executor&.prototype?(name)
|
|
60
|
+
return ENV[name] if ENV.key?(name)
|
|
61
|
+
raise RuntimeError, "undefined value #{name}" if @state.options['nounset']
|
|
62
|
+
''
|
|
63
|
+
when :env
|
|
64
|
+
name = ast[1]
|
|
65
|
+
return @state.local_get(name) if @state.local_defined?(name)
|
|
66
|
+
return ENV[name] if ENV.key?(name)
|
|
67
|
+
raise RuntimeError, "undefined environment variable $#{name}" if @state.options['nounset']
|
|
68
|
+
''
|
|
69
|
+
when :positional then @state.local_get("$#{ast[1]}") || ''
|
|
70
|
+
when :status then @state.last_status
|
|
71
|
+
when :list then ast[1].map { |x| eval_ast(x) }
|
|
72
|
+
when :map then ast[1].to_h { |k, v| [eval_ast(k), eval_ast(v)] }
|
|
73
|
+
when :unary then unary(ast[1], eval_ast(ast[2]))
|
|
74
|
+
when :binary then binary(ast[1], ast[2], ast[3])
|
|
75
|
+
when :index then index(eval_ast(ast[1]), eval_ast(ast[2]))
|
|
76
|
+
when :safe_index
|
|
77
|
+
owner = eval_ast(ast[1])
|
|
78
|
+
safe_index(owner, owner.nil? ? nil : eval_ast(ast[2]))
|
|
79
|
+
when :member then member(eval_ast(ast[1]), ast[2])
|
|
80
|
+
when :safe_member
|
|
81
|
+
owner = eval_ast(ast[1])
|
|
82
|
+
safe_member(owner, ast[2])
|
|
83
|
+
when :call then call(ast[1], ast[2].map { |a| eval_ast(a) })
|
|
84
|
+
else raise RuntimeError, "unknown AST node #{kind.inspect}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def call(callee_ast, args)
|
|
89
|
+
if callee_ast[0] == :local
|
|
90
|
+
name = callee_ast[1]
|
|
91
|
+
return @executor.instantiate(name, args) if @executor&.prototype?(name)
|
|
92
|
+
return @executor.call_function(name, args) if @executor&.function?(name)
|
|
93
|
+
return builtin_function(name, args) if builtin_function?(name)
|
|
94
|
+
end
|
|
95
|
+
fn = eval_ast(callee_ast)
|
|
96
|
+
invoke_callable(fn, args)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def format(value) = stringify(value)
|
|
100
|
+
def get_member_value(value, key) = member(value, key)
|
|
101
|
+
|
|
102
|
+
def set_member_value(value, key, new_value)
|
|
103
|
+
case value
|
|
104
|
+
when ObjectValue
|
|
105
|
+
value.set(key, new_value)
|
|
106
|
+
when NamespaceValue
|
|
107
|
+
raise RuntimeError, 'worker tasks cannot mutate a shared space; use an atom/object/channel' if @state.worker_thread?
|
|
108
|
+
value.set(key, new_value)
|
|
109
|
+
when Hash
|
|
110
|
+
actual = if value.key?(key)
|
|
111
|
+
key
|
|
112
|
+
elsif value.key?(key.to_sym)
|
|
113
|
+
key.to_sym
|
|
114
|
+
else
|
|
115
|
+
key
|
|
116
|
+
end
|
|
117
|
+
value[actual] = new_value
|
|
118
|
+
else
|
|
119
|
+
raise RuntimeError, "cannot assign .#{key} on #{type_name(value)}"
|
|
120
|
+
end
|
|
121
|
+
new_value
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Build a thread-isolated copy of ordinary RSH data. Explicit
|
|
125
|
+
# synchronization values stay shared by reference; plain collections do not.
|
|
126
|
+
def worker_snapshot(value)
|
|
127
|
+
isolate_for_worker(value, {})
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def isolate_for_worker(value, seen)
|
|
131
|
+
case value
|
|
132
|
+
when String
|
|
133
|
+
value.dup
|
|
134
|
+
when Array
|
|
135
|
+
return seen[value.object_id] if seen.key?(value.object_id)
|
|
136
|
+
copy = []
|
|
137
|
+
seen[value.object_id] = copy
|
|
138
|
+
value.each { |item| copy << isolate_for_worker(item, seen) }
|
|
139
|
+
copy
|
|
140
|
+
when Hash
|
|
141
|
+
return seen[value.object_id] if seen.key?(value.object_id)
|
|
142
|
+
copy = {}
|
|
143
|
+
seen[value.object_id] = copy
|
|
144
|
+
value.each do |key, item|
|
|
145
|
+
copy[isolate_for_worker(key, seen)] = isolate_for_worker(item, seen)
|
|
146
|
+
end
|
|
147
|
+
copy
|
|
148
|
+
when LambdaValue
|
|
149
|
+
LambdaValue.new(value.params, value.body, isolate_for_worker(value.captured, seen).freeze)
|
|
150
|
+
when FunctionRef
|
|
151
|
+
captured = value.captured ? isolate_for_worker(value.captured, seen).freeze : nil
|
|
152
|
+
FunctionRef.new(value.name, captured)
|
|
153
|
+
when Range
|
|
154
|
+
Range.new(isolate_for_worker(value.begin, seen), isolate_for_worker(value.end, seen), value.exclude_end?)
|
|
155
|
+
# These are deliberately shareable or immutable handles.
|
|
156
|
+
when ObjectValue, TaskValue, ChannelValue, AtomValue, NamespaceValue,
|
|
157
|
+
NativeFunctionValue, NativePointerValue, CBufferValue, CommandValue, PrototypeRef, BoundMethodValue, NativeMethodValue, CodeValue,
|
|
158
|
+
Integer, Float, TrueClass, FalseClass, NilClass, Symbol
|
|
159
|
+
value
|
|
160
|
+
else
|
|
161
|
+
value.frozen? ? value : (value.dup rescue value)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
BUILTIN_FUNCTIONS = %w[
|
|
168
|
+
int float str bool len empty contains starts ends env rand pick status type round floor ceil sqrt clamp
|
|
169
|
+
keys values join split upper lower trim abs min max cwd clock capture sh
|
|
170
|
+
map filter reject fold find any all count sum sort uniq flat zip enumerate
|
|
171
|
+
each take drop chunk group tap partial compose
|
|
172
|
+
spawn await await_all race parallel pmap chan atom sleep cpu_count
|
|
173
|
+
cmd attempt fail assert clone fields methods protoof is
|
|
174
|
+
eval code run sourceof valid locals fns protos traits
|
|
175
|
+
readfile writefile appendfile exists file dir glob basename dirname ext
|
|
176
|
+
json json_dump lines words replace starts_with ends_with shellquote stat mkdirp rmfile cpfile mvfile
|
|
177
|
+
cbuf
|
|
178
|
+
].freeze
|
|
179
|
+
|
|
180
|
+
def builtin_function?(name) = BUILTIN_FUNCTIONS.include?(name)
|
|
181
|
+
|
|
182
|
+
def builtin_function(name, args)
|
|
183
|
+
case name
|
|
184
|
+
when 'int' then numeric(args[0]).to_i
|
|
185
|
+
when 'float' then numeric(args[0]).to_f
|
|
186
|
+
when 'str' then stringify(args[0])
|
|
187
|
+
when 'bool' then truthy?(args[0])
|
|
188
|
+
when 'len' then args[0].respond_to?(:length) ? args[0].length : stringify(args[0]).length
|
|
189
|
+
when 'empty' then args[0].respond_to?(:empty?) ? args[0].empty? : stringify(args[0]).empty?
|
|
190
|
+
when 'contains' then contains(args[0], args[1])
|
|
191
|
+
when 'starts' then stringify(args[0]).start_with?(stringify(args[1]))
|
|
192
|
+
when 'ends' then stringify(args[0]).end_with?(stringify(args[1]))
|
|
193
|
+
when 'env' then ENV[stringify(args[0])] || ''
|
|
194
|
+
when 'rand' then Kernel.rand([numeric(args[0]).to_i, 1].max)
|
|
195
|
+
when 'pick' then args.empty? ? nil : args.sample
|
|
196
|
+
when 'status' then @state.last_status
|
|
197
|
+
when 'type' then type_name(args[0])
|
|
198
|
+
when 'round' then numeric(args[0]).round(args[1] ? numeric(args[1]).to_i : 0)
|
|
199
|
+
when 'floor' then numeric(args[0]).floor
|
|
200
|
+
when 'ceil' then numeric(args[0]).ceil
|
|
201
|
+
when 'sqrt'
|
|
202
|
+
x = numeric(args[0])
|
|
203
|
+
raise RuntimeError, 'sqrt domain error' if x.negative?
|
|
204
|
+
Math.sqrt(x)
|
|
205
|
+
when 'clamp'
|
|
206
|
+
x, lo, hi = numeric(args[0]), numeric(args[1]), numeric(args[2])
|
|
207
|
+
raise RuntimeError, 'clamp lower bound is greater than upper bound' if lo > hi
|
|
208
|
+
[[x, lo].max, hi].min
|
|
209
|
+
when 'keys' then args[0].is_a?(Hash) ? args[0].keys : []
|
|
210
|
+
when 'values' then args[0].is_a?(Hash) ? args[0].values : []
|
|
211
|
+
when 'join' then Array(args[0]).join(stringify(args[1] || ''))
|
|
212
|
+
when 'split' then stringify(args[0]).split(args[1] ? stringify(args[1]) : nil)
|
|
213
|
+
when 'upper' then stringify(args[0]).upcase
|
|
214
|
+
when 'lower' then stringify(args[0]).downcase
|
|
215
|
+
when 'trim' then stringify(args[0]).strip
|
|
216
|
+
when 'abs' then numeric(args[0]).abs
|
|
217
|
+
when 'min' then args.compact.min
|
|
218
|
+
when 'max' then args.compact.max
|
|
219
|
+
when 'cwd' then Dir.pwd
|
|
220
|
+
when 'clock' then Time.now.to_f
|
|
221
|
+
when 'capture'
|
|
222
|
+
require_executor!('capture')
|
|
223
|
+
@executor.capture(stringify(args[0]))
|
|
224
|
+
when 'sh'
|
|
225
|
+
require_executor!('sh')
|
|
226
|
+
raise RuntimeError, 'sh() cannot take foreground job control from an RSH worker; use capture()' if @state.worker_thread?
|
|
227
|
+
@executor.execute_line(stringify(args[0]))
|
|
228
|
+
|
|
229
|
+
when 'map'
|
|
230
|
+
sequence_map(args[0]) { |*item| invoke_callable(args[1], item) }
|
|
231
|
+
when 'filter'
|
|
232
|
+
sequence_select(args[0]) { |*item| truthy?(invoke_callable(args[1], item)) }
|
|
233
|
+
when 'reject'
|
|
234
|
+
sequence_select(args[0]) { |*item| !truthy?(invoke_callable(args[1], item)) }
|
|
235
|
+
when 'fold'
|
|
236
|
+
acc = args[1]
|
|
237
|
+
sequence_each(args[0]) { |*item| acc = invoke_callable(args[2], [acc, *item]) }
|
|
238
|
+
acc
|
|
239
|
+
when 'find'
|
|
240
|
+
found = nil
|
|
241
|
+
hit = false
|
|
242
|
+
sequence_each(args[0]) do |*item|
|
|
243
|
+
next unless truthy?(invoke_callable(args[1], item))
|
|
244
|
+
found = item.length == 1 ? item[0] : item
|
|
245
|
+
hit = true
|
|
246
|
+
break
|
|
247
|
+
end
|
|
248
|
+
hit ? found : nil
|
|
249
|
+
when 'any'
|
|
250
|
+
fn = args[1]
|
|
251
|
+
result = false
|
|
252
|
+
sequence_each(args[0]) do |*item|
|
|
253
|
+
value = fn ? invoke_callable(fn, item) : (item.length == 1 ? item[0] : item)
|
|
254
|
+
if truthy?(value)
|
|
255
|
+
result = true
|
|
256
|
+
break
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
result
|
|
260
|
+
when 'all'
|
|
261
|
+
fn = args[1]
|
|
262
|
+
result = true
|
|
263
|
+
sequence_each(args[0]) do |*item|
|
|
264
|
+
value = fn ? invoke_callable(fn, item) : (item.length == 1 ? item[0] : item)
|
|
265
|
+
unless truthy?(value)
|
|
266
|
+
result = false
|
|
267
|
+
break
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
result
|
|
271
|
+
when 'count'
|
|
272
|
+
fn = args[1]
|
|
273
|
+
n = 0
|
|
274
|
+
sequence_each(args[0]) do |*item|
|
|
275
|
+
value = fn ? invoke_callable(fn, item) : (item.length == 1 ? item[0] : item)
|
|
276
|
+
n += 1 if fn.nil? || truthy?(value)
|
|
277
|
+
end
|
|
278
|
+
n
|
|
279
|
+
when 'sum'
|
|
280
|
+
fn = args[1]
|
|
281
|
+
total = 0
|
|
282
|
+
sequence_each(args[0]) do |*item|
|
|
283
|
+
value = fn ? invoke_callable(fn, item) : (item.length == 1 ? item[0] : item)
|
|
284
|
+
total += numeric(value)
|
|
285
|
+
end
|
|
286
|
+
total
|
|
287
|
+
when 'sort'
|
|
288
|
+
seq = sequence_values(args[0])
|
|
289
|
+
args[1] ? seq.sort_by { |item| invoke_callable(args[1], [item]) } : seq.sort
|
|
290
|
+
when 'uniq' then sequence_values(args[0]).uniq
|
|
291
|
+
when 'flat' then sequence_values(args[0]).flatten(args[1] ? numeric(args[1]).to_i : 1)
|
|
292
|
+
when 'zip'
|
|
293
|
+
left = sequence_values(args[0])
|
|
294
|
+
right = sequence_values(args[1])
|
|
295
|
+
left.zip(right)
|
|
296
|
+
when 'enumerate'
|
|
297
|
+
sequence_values(args[0]).each_with_index.map { |value, index| [index, value] }
|
|
298
|
+
when 'each'
|
|
299
|
+
sequence_each(args[0]) { |*item| invoke_callable(args[1], item) }
|
|
300
|
+
args[0]
|
|
301
|
+
when 'take' then sequence_values(args[0]).first(numeric(args[1] || 1).to_i)
|
|
302
|
+
when 'drop' then sequence_values(args[0]).drop(numeric(args[1] || 1).to_i)
|
|
303
|
+
when 'chunk'
|
|
304
|
+
n = numeric(args[1] || 1).to_i
|
|
305
|
+
raise RuntimeError, 'chunk size must be positive' if n <= 0
|
|
306
|
+
sequence_values(args[0]).each_slice(n).to_a
|
|
307
|
+
when 'group'
|
|
308
|
+
fn = args[1]
|
|
309
|
+
sequence_values(args[0]).group_by { |item| invoke_callable(fn, [item]) }
|
|
310
|
+
when 'tap'
|
|
311
|
+
invoke_callable(args[1], [args[0]])
|
|
312
|
+
args[0]
|
|
313
|
+
when 'partial'
|
|
314
|
+
fn = args[0]
|
|
315
|
+
prefix = args[1..].freeze
|
|
316
|
+
proc { |*rest| invoke_callable(fn, [*prefix, *rest]) }
|
|
317
|
+
when 'compose'
|
|
318
|
+
fns = args.freeze
|
|
319
|
+
proc do |*input|
|
|
320
|
+
raise RuntimeError, 'compose() needs at least one callable' if fns.empty?
|
|
321
|
+
value = invoke_callable(fns[-1], input)
|
|
322
|
+
fns[0...-1].reverse_each { |fn| value = invoke_callable(fn, [value]) }
|
|
323
|
+
value
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
when 'spawn'
|
|
327
|
+
fn = args[0]
|
|
328
|
+
spawn_task(fn, args[1..])
|
|
329
|
+
when 'await'
|
|
330
|
+
task = args[0]
|
|
331
|
+
raise RuntimeError, 'await() expects a task' unless task.is_a?(TaskValue)
|
|
332
|
+
task.await(args[1])
|
|
333
|
+
when 'await_all'
|
|
334
|
+
tasks = sequence_values(args[0])
|
|
335
|
+
raise RuntimeError, 'await_all() expects tasks' unless tasks.all? { |t| t.is_a?(TaskValue) }
|
|
336
|
+
values = []
|
|
337
|
+
begin
|
|
338
|
+
tasks.each { |task| values << task.await }
|
|
339
|
+
rescue StandardError
|
|
340
|
+
tasks.each { |task| task.cancel unless task.done? }
|
|
341
|
+
raise
|
|
342
|
+
end
|
|
343
|
+
values
|
|
344
|
+
when 'race'
|
|
345
|
+
tasks = sequence_values(args[0])
|
|
346
|
+
raise RuntimeError, 'race() requires at least one task' if tasks.empty?
|
|
347
|
+
raise RuntimeError, 'race() expects tasks' unless tasks.all? { |t| t.is_a?(TaskValue) }
|
|
348
|
+
winner = nil
|
|
349
|
+
begin
|
|
350
|
+
loop do
|
|
351
|
+
winner = tasks.find(&:done?)
|
|
352
|
+
break if winner
|
|
353
|
+
Kernel.sleep(0.001)
|
|
354
|
+
end
|
|
355
|
+
winner.await
|
|
356
|
+
ensure
|
|
357
|
+
tasks.each { |task| task.cancel if task != winner && !task.done? }
|
|
358
|
+
end
|
|
359
|
+
when 'parallel'
|
|
360
|
+
parallel_map(args[0], args[1], args[2])
|
|
361
|
+
when 'pmap'
|
|
362
|
+
process_map(args[0], args[1], args[2])
|
|
363
|
+
when 'chan' then ChannelValue.new(args[0] ? numeric(args[0]).to_i : 0)
|
|
364
|
+
when 'atom' then AtomValue.new(args[0])
|
|
365
|
+
when 'sleep'
|
|
366
|
+
Kernel.sleep(numeric(args[0] || 0).to_f)
|
|
367
|
+
nil
|
|
368
|
+
when 'cpu_count' then Etc.nprocessors
|
|
369
|
+
when 'cmd'
|
|
370
|
+
argv = args.length == 1 && args[0].is_a?(Array) ? args[0] : args
|
|
371
|
+
CommandValue.new(argv.map { |part| stringify(part) })
|
|
372
|
+
when 'attempt'
|
|
373
|
+
begin
|
|
374
|
+
{ 'ok' => true, 'value' => invoke_callable(args[0], args[1..]), 'error' => nil }
|
|
375
|
+
rescue StandardError => e
|
|
376
|
+
{ 'ok' => false, 'value' => nil, 'error' => { 'type' => e.class.name, 'message' => e.message } }
|
|
377
|
+
end
|
|
378
|
+
when 'fail'
|
|
379
|
+
raise RuntimeError, stringify(args[0] || 'failure')
|
|
380
|
+
when 'assert'
|
|
381
|
+
raise RuntimeError, stringify(args[1] || 'assertion failed') unless truthy?(args[0])
|
|
382
|
+
args[0]
|
|
383
|
+
|
|
384
|
+
when 'clone'
|
|
385
|
+
value = args[0]
|
|
386
|
+
value.is_a?(ObjectValue) ? value.copy : (value.dup rescue value)
|
|
387
|
+
when 'fields'
|
|
388
|
+
value = args[0]
|
|
389
|
+
value.is_a?(ObjectValue) ? value.fields : (value.is_a?(Hash) ? value.dup : {})
|
|
390
|
+
when 'methods'
|
|
391
|
+
value = args[0]
|
|
392
|
+
value.is_a?(ObjectValue) && @executor ? @executor.prototype_methods(value.proto_name) : native_methods_for(value)
|
|
393
|
+
when 'protoof'
|
|
394
|
+
value = args[0]
|
|
395
|
+
value.is_a?(ObjectValue) ? PrototypeRef.new(value.proto_name) : nil
|
|
396
|
+
when 'is'
|
|
397
|
+
value, proto = args[0], args[1]
|
|
398
|
+
pname = proto.is_a?(PrototypeRef) ? proto.name : stringify(proto)
|
|
399
|
+
value.is_a?(ObjectValue) && value.proto_name == pname
|
|
400
|
+
|
|
401
|
+
when 'eval'
|
|
402
|
+
parse_eval(stringify(args[0]))
|
|
403
|
+
when 'code'
|
|
404
|
+
source = stringify(args[0])
|
|
405
|
+
CodeValue.new(source, ProgramParser.new(source).parse.freeze)
|
|
406
|
+
when 'run'
|
|
407
|
+
require_executor!('run')
|
|
408
|
+
value = args[0]
|
|
409
|
+
nodes = value.is_a?(CodeValue) ? value.nodes : ProgramParser.new(stringify(value)).parse
|
|
410
|
+
@executor.run_program(nodes)
|
|
411
|
+
when 'sourceof'
|
|
412
|
+
value = args[0]
|
|
413
|
+
value.is_a?(CodeValue) ? value.source : stringify(value)
|
|
414
|
+
when 'valid'
|
|
415
|
+
source = stringify(args[0])
|
|
416
|
+
mode = stringify(args[1] || 'program')
|
|
417
|
+
begin
|
|
418
|
+
mode == 'expr' ? ExprParser.new(source).parse : ProgramParser.new(source).parse
|
|
419
|
+
true
|
|
420
|
+
rescue ParseError
|
|
421
|
+
false
|
|
422
|
+
end
|
|
423
|
+
when 'locals' then @state.locals_snapshot
|
|
424
|
+
when 'fns' then @state.functions.keys.sort
|
|
425
|
+
when 'protos' then @state.prototypes.keys.sort
|
|
426
|
+
when 'traits' then @state.traits.keys.sort
|
|
427
|
+
|
|
428
|
+
when 'readfile'
|
|
429
|
+
path = stringify(args[0])
|
|
430
|
+
size = File.size(path)
|
|
431
|
+
raise RuntimeError, "readfile: file exceeds #{MAX_FILE_READ} bytes" if size > MAX_FILE_READ
|
|
432
|
+
File.binread(path)
|
|
433
|
+
when 'writefile'
|
|
434
|
+
path = stringify(args[0])
|
|
435
|
+
data = stringify(args[1])
|
|
436
|
+
File.binwrite(path, data)
|
|
437
|
+
when 'appendfile'
|
|
438
|
+
path = stringify(args[0])
|
|
439
|
+
data = stringify(args[1])
|
|
440
|
+
File.open(path, 'ab') { |f| f.write(data) }
|
|
441
|
+
when 'exists' then File.exist?(stringify(args[0]))
|
|
442
|
+
when 'file' then File.file?(stringify(args[0]))
|
|
443
|
+
when 'dir' then File.directory?(stringify(args[0]))
|
|
444
|
+
when 'glob' then Dir.glob(stringify(args[0])).sort
|
|
445
|
+
when 'basename' then File.basename(stringify(args[0]))
|
|
446
|
+
when 'dirname' then File.dirname(stringify(args[0]))
|
|
447
|
+
when 'ext' then File.extname(stringify(args[0]))
|
|
448
|
+
when 'json' then JSON.parse(stringify(args[0]))
|
|
449
|
+
when 'json_dump' then JSON.generate(args[0])
|
|
450
|
+
when 'lines' then stringify(args[0]).lines(chomp: true)
|
|
451
|
+
when 'words' then stringify(args[0]).split
|
|
452
|
+
when 'replace' then stringify(args[0]).gsub(stringify(args[1]), stringify(args[2]))
|
|
453
|
+
when 'starts_with' then stringify(args[0]).start_with?(stringify(args[1]))
|
|
454
|
+
when 'ends_with' then stringify(args[0]).end_with?(stringify(args[1]))
|
|
455
|
+
when 'shellquote' then Shellwords.escape(stringify(args[0]))
|
|
456
|
+
when 'stat'
|
|
457
|
+
st = File.stat(stringify(args[0]))
|
|
458
|
+
{ 'size' => st.size, 'mode' => st.mode, 'uid' => st.uid, 'gid' => st.gid,
|
|
459
|
+
'mtime' => st.mtime.to_f, 'file' => st.file?, 'dir' => st.directory?, 'symlink' => st.symlink? }
|
|
460
|
+
when 'mkdirp' then FileUtils.mkdir_p(stringify(args[0])); stringify(args[0])
|
|
461
|
+
when 'rmfile' then FileUtils.rm_f(stringify(args[0])); true
|
|
462
|
+
when 'cpfile' then FileUtils.cp(stringify(args[0]), stringify(args[1])); stringify(args[1])
|
|
463
|
+
when 'mvfile' then FileUtils.mv(stringify(args[0]), stringify(args[1])); stringify(args[1])
|
|
464
|
+
when 'cbuf' then CBufferValue.new(numeric(args[0]).to_i)
|
|
465
|
+
else raise RuntimeError, "unknown function #{name}"
|
|
466
|
+
end
|
|
467
|
+
rescue SystemCallError => e
|
|
468
|
+
raise RuntimeError, "#{name}: #{e.message}"
|
|
469
|
+
rescue JSON::ParserError, JSON::GeneratorError => e
|
|
470
|
+
raise RuntimeError, "#{name}: #{e.message}"
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def require_executor!(feature)
|
|
474
|
+
raise RuntimeError, "#{feature}() unavailable here" unless @executor
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def invoke_callable(fn, args)
|
|
478
|
+
case fn
|
|
479
|
+
when LambdaValue
|
|
480
|
+
call_lambda(fn, args)
|
|
481
|
+
when FunctionRef
|
|
482
|
+
require_executor!('function call')
|
|
483
|
+
@executor.call_function(fn.name, args, call_seed: fn.captured)
|
|
484
|
+
when BoundMethodValue
|
|
485
|
+
require_executor!('method call')
|
|
486
|
+
@executor.call_method(fn.receiver, fn.name, args)
|
|
487
|
+
when NativeMethodValue
|
|
488
|
+
native_method(fn.receiver, fn.name, args)
|
|
489
|
+
when PrototypeRef
|
|
490
|
+
require_executor!('prototype construction')
|
|
491
|
+
@executor.instantiate(fn.name, args)
|
|
492
|
+
when Proc, Method
|
|
493
|
+
fn.call(*args)
|
|
494
|
+
when String
|
|
495
|
+
if @executor&.function?(fn)
|
|
496
|
+
@executor.call_function(fn, args)
|
|
497
|
+
elsif builtin_function?(fn)
|
|
498
|
+
builtin_function(fn, args)
|
|
499
|
+
else
|
|
500
|
+
raise RuntimeError, "unknown callable #{fn.inspect}"
|
|
501
|
+
end
|
|
502
|
+
else
|
|
503
|
+
if fn.respond_to?(:call)
|
|
504
|
+
fn.call(*args)
|
|
505
|
+
else
|
|
506
|
+
raise RuntimeError, 'value is not callable'
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def call_lambda(fn, args)
|
|
512
|
+
key = :"srsh_lambda_depth_#{object_id}"
|
|
513
|
+
depth = Thread.current[key].to_i
|
|
514
|
+
raise RuntimeError, 'lambda call depth exceeded' if depth >= MAX_LAMBDA_DEPTH
|
|
515
|
+
Thread.current[key] = depth + 1
|
|
516
|
+
pushed = false
|
|
517
|
+
scope = fn.captured.dup
|
|
518
|
+
args.each_with_index { |value, index| scope["$#{index + 1}"] = value }
|
|
519
|
+
arg_index = 0
|
|
520
|
+
fn.params.each do |param|
|
|
521
|
+
if param.start_with?('*')
|
|
522
|
+
scope[param[1..]] = args[arg_index..] || []
|
|
523
|
+
arg_index = args.length
|
|
524
|
+
else
|
|
525
|
+
scope[param] = args[arg_index]
|
|
526
|
+
arg_index += 1
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
scope['$0'] = '<lambda>'
|
|
530
|
+
@state.push_scope(scope)
|
|
531
|
+
pushed = true
|
|
532
|
+
eval_ast(fn.body)
|
|
533
|
+
ensure
|
|
534
|
+
@state.pop_scope if pushed
|
|
535
|
+
Thread.current[key] = [Thread.current[key].to_i - 1, 0].max if defined?(key)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def unary(op, value)
|
|
539
|
+
case op
|
|
540
|
+
when '-' then -numeric(value)
|
|
541
|
+
when '+' then numeric(value)
|
|
542
|
+
when 'not', '!' then !truthy?(value)
|
|
543
|
+
else raise RuntimeError, "unknown unary operator #{op}"
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def binary(op, left_ast, right_ast)
|
|
548
|
+
if op == '|>'
|
|
549
|
+
left = eval_ast(left_ast)
|
|
550
|
+
return pipe(left, right_ast)
|
|
551
|
+
elsif op == 'and' || op == '&&'
|
|
552
|
+
left = eval_ast(left_ast)
|
|
553
|
+
return left unless truthy?(left)
|
|
554
|
+
return eval_ast(right_ast)
|
|
555
|
+
elsif op == 'or' || op == '||'
|
|
556
|
+
left = eval_ast(left_ast)
|
|
557
|
+
return left if truthy?(left)
|
|
558
|
+
return eval_ast(right_ast)
|
|
559
|
+
elsif op == '??'
|
|
560
|
+
left = eval_ast(left_ast)
|
|
561
|
+
return left unless left.nil?
|
|
562
|
+
return eval_ast(right_ast)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
a = eval_ast(left_ast)
|
|
566
|
+
b = eval_ast(right_ast)
|
|
567
|
+
case op
|
|
568
|
+
when '+' then arithmetic_add(a, b)
|
|
569
|
+
when '++' then stringify(a) + stringify(b)
|
|
570
|
+
when '-' then numeric(a) - numeric(b)
|
|
571
|
+
when '*' then numeric(a) * numeric(b)
|
|
572
|
+
when '/'
|
|
573
|
+
d = numeric(b)
|
|
574
|
+
raise RuntimeError, 'division by zero' if d.zero?
|
|
575
|
+
numeric(a).fdiv(d)
|
|
576
|
+
when '%'
|
|
577
|
+
d = numeric(b)
|
|
578
|
+
raise RuntimeError, 'modulo by zero' if d.zero?
|
|
579
|
+
numeric(a) % d
|
|
580
|
+
when '**' then numeric(a)**numeric(b)
|
|
581
|
+
when '==' then compare(a, b).zero?
|
|
582
|
+
when '!=' then !compare(a, b).zero?
|
|
583
|
+
when '===' then a.eql?(b)
|
|
584
|
+
when '!==' then !a.eql?(b)
|
|
585
|
+
when '<' then compare(a, b).negative?
|
|
586
|
+
when '<=' then compare(a, b) <= 0
|
|
587
|
+
when '>' then compare(a, b).positive?
|
|
588
|
+
when '>=' then compare(a, b) >= 0
|
|
589
|
+
when '=~' then !!(stringify(a) =~ Regexp.new(stringify(b)))
|
|
590
|
+
when '!~' then !(stringify(a) =~ Regexp.new(stringify(b)))
|
|
591
|
+
when 'in' then membership(a, b)
|
|
592
|
+
when '..' then Range.new(numeric_or_same(a), numeric_or_same(b), false)
|
|
593
|
+
when '..<' then Range.new(numeric_or_same(a), numeric_or_same(b), true)
|
|
594
|
+
else raise RuntimeError, "unknown operator #{op}"
|
|
595
|
+
end
|
|
596
|
+
rescue RegexpError => e
|
|
597
|
+
raise RuntimeError, "bad regex: #{e.message}"
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def pipe(value, right_ast)
|
|
601
|
+
case right_ast[0]
|
|
602
|
+
when :call
|
|
603
|
+
callee = right_ast[1]
|
|
604
|
+
args = [value] + right_ast[2].map { |arg| eval_ast(arg) }
|
|
605
|
+
call(callee, args)
|
|
606
|
+
when :local
|
|
607
|
+
name = right_ast[1]
|
|
608
|
+
return @executor.call_function(name, [value]) if @executor&.function?(name)
|
|
609
|
+
return builtin_function(name, [value]) if builtin_function?(name)
|
|
610
|
+
invoke_callable(eval_ast(right_ast), [value])
|
|
611
|
+
when :lambda
|
|
612
|
+
invoke_callable(eval_ast(right_ast), [value])
|
|
613
|
+
else
|
|
614
|
+
invoke_callable(eval_ast(right_ast), [value])
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def sequence_each(value, &block)
|
|
619
|
+
case value
|
|
620
|
+
when Hash
|
|
621
|
+
value.each { |k, v| block.call(k, v) }
|
|
622
|
+
when Range, Array
|
|
623
|
+
value.each { |item| block.call(item) }
|
|
624
|
+
when String
|
|
625
|
+
value.each_line(chomp: true) { |line| block.call(line) }
|
|
626
|
+
else
|
|
627
|
+
raise RuntimeError, "expected iterable, got #{type_name(value)}"
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def sequence_map(value)
|
|
632
|
+
out = []
|
|
633
|
+
sequence_each(value) { |*item| out << yield(*item) }
|
|
634
|
+
out
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def sequence_select(value)
|
|
638
|
+
if value.is_a?(Hash)
|
|
639
|
+
out = {}
|
|
640
|
+
sequence_each(value) { |k, v| out[k] = v if yield(k, v) }
|
|
641
|
+
out
|
|
642
|
+
else
|
|
643
|
+
out = []
|
|
644
|
+
sequence_each(value) do |*item|
|
|
645
|
+
value = item.length == 1 ? item[0] : item
|
|
646
|
+
out << value if yield(*item)
|
|
647
|
+
end
|
|
648
|
+
out
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def sequence_values(value)
|
|
653
|
+
case value
|
|
654
|
+
when Hash then value.to_a
|
|
655
|
+
when Range, Array then value.to_a
|
|
656
|
+
when String then value.lines(chomp: true)
|
|
657
|
+
else raise RuntimeError, "expected iterable, got #{type_name(value)}"
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def compare(a, b)
|
|
662
|
+
if numeric_candidate?(a) && numeric_candidate?(b)
|
|
663
|
+
numeric(a) <=> numeric(b)
|
|
664
|
+
else
|
|
665
|
+
stringify(a) <=> stringify(b)
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def numeric_candidate?(value)
|
|
670
|
+
return true if value.is_a?(Numeric)
|
|
671
|
+
value.to_s.strip.match?(/\A[+-]?(?:\d+|\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?\z/)
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def numeric(value)
|
|
675
|
+
return value if value.is_a?(Numeric)
|
|
676
|
+
s = value.to_s.strip
|
|
677
|
+
return s.to_i if s.match?(/\A[+-]?\d+\z/)
|
|
678
|
+
return s.to_f if s.match?(/\A[+-]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?\z/)
|
|
679
|
+
raise RuntimeError, "expected number, got #{value.inspect}"
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def numeric_or_same(value)
|
|
683
|
+
numeric(value)
|
|
684
|
+
rescue RuntimeError
|
|
685
|
+
value
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def stringify(value)
|
|
689
|
+
case value
|
|
690
|
+
when nil then ''
|
|
691
|
+
when true then 'yes'
|
|
692
|
+
when false then 'no'
|
|
693
|
+
when CodeValue then value.source
|
|
694
|
+
when Array then '[' + value.map { |v| repr(v) }.join(', ') + ']'
|
|
695
|
+
when Hash then '%[' + value.map { |k, v| "#{repr_key(k)}: #{repr(v)}" }.join(', ') + ']'
|
|
696
|
+
else value.to_s
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def repr(value)
|
|
701
|
+
case value
|
|
702
|
+
when nil then 'void'
|
|
703
|
+
when true then 'yes'
|
|
704
|
+
when false then 'no'
|
|
705
|
+
when String then value.inspect
|
|
706
|
+
when Array then '[' + value.map { |v| repr(v) }.join(', ') + ']'
|
|
707
|
+
when Hash then '%[' + value.map { |k, v| "#{repr_key(k)}: #{repr(v)}" }.join(', ') + ']'
|
|
708
|
+
when Range then "#{repr(value.begin)}#{value.exclude_end? ? '..<' : '..'}#{repr(value.end)}"
|
|
709
|
+
else value.to_s
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def repr_key(key)
|
|
714
|
+
key.is_a?(String) && key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/) ? key : repr(key)
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def arithmetic_add(a, b)
|
|
718
|
+
return a + b if a.is_a?(Numeric) && b.is_a?(Numeric)
|
|
719
|
+
numeric(a) + numeric(b)
|
|
720
|
+
rescue RuntimeError
|
|
721
|
+
stringify(a) + stringify(b)
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def membership(a, b)
|
|
725
|
+
case b
|
|
726
|
+
when Range, Array, Hash, String then b.include?(a)
|
|
727
|
+
else false
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def contains(a, b)
|
|
732
|
+
case a
|
|
733
|
+
when Hash then a.key?(b)
|
|
734
|
+
else a.respond_to?(:include?) ? a.include?(b) : stringify(a).include?(stringify(b))
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def index(value, key)
|
|
739
|
+
raise RuntimeError, "cannot index #{type_name(value)}" unless value.respond_to?(:[])
|
|
740
|
+
value[key]
|
|
741
|
+
rescue TypeError, IndexError => e
|
|
742
|
+
raise RuntimeError, "bad index: #{e.message}"
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def safe_index(value, key)
|
|
746
|
+
return nil if value.nil? || !value.respond_to?(:[])
|
|
747
|
+
value[key]
|
|
748
|
+
rescue StandardError
|
|
749
|
+
nil
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def member(value, key)
|
|
753
|
+
key = key.to_s
|
|
754
|
+
if value.is_a?(CommandValue)
|
|
755
|
+
return NativeMethodValue.new(value, key) if %w[capture result check run task argv].include?(key)
|
|
756
|
+
raise RuntimeError, "command has no member .#{key}"
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
if value.is_a?(NamespaceValue)
|
|
760
|
+
return value.get(key) if value.member?(key)
|
|
761
|
+
return NativeMethodValue.new(value, key) if %w[keys has].include?(key)
|
|
762
|
+
raise RuntimeError, "space #{value.name} has no member .#{key}"
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
if value.is_a?(ObjectValue)
|
|
766
|
+
return value.get(key) if value.field?(key)
|
|
767
|
+
return BoundMethodValue.new(value, key) if @executor&.object_method?(value, key)
|
|
768
|
+
return NativeMethodValue.new(value, key) if %w[fields methods proto clone is].include?(key)
|
|
769
|
+
raise RuntimeError, "#{value.proto_name} has no member .#{key}"
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
if value.is_a?(PrototypeRef)
|
|
773
|
+
return NativeMethodValue.new(value, key) if key == 'new'
|
|
774
|
+
raise RuntimeError, "prototype #{value.name} has no member .#{key}"
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
if value.is_a?(Hash)
|
|
778
|
+
return value[key] if value.key?(key)
|
|
779
|
+
sym = key.to_sym
|
|
780
|
+
return value[sym] if value.key?(sym)
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
return NativeMethodValue.new(value, key) if native_methods_for(value).include?(key)
|
|
784
|
+
raise RuntimeError, "cannot access .#{key} on #{type_name(value)}"
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def safe_member(value, key)
|
|
788
|
+
return nil if value.nil?
|
|
789
|
+
member(value, key)
|
|
790
|
+
rescue RuntimeError
|
|
791
|
+
nil
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def native_methods_for(value)
|
|
795
|
+
case value
|
|
796
|
+
when CommandValue then %w[capture result check run task argv]
|
|
797
|
+
when NamespaceValue then %w[keys has]
|
|
798
|
+
when ObjectValue then %w[fields methods proto clone is]
|
|
799
|
+
when TaskValue then %w[await done status cancel]
|
|
800
|
+
when ChannelValue then %w[send recv try_recv close closed size]
|
|
801
|
+
when AtomValue then %w[get set swap]
|
|
802
|
+
when CBufferValue then %w[size address read write string clear ptr]
|
|
803
|
+
when NativePointerValue then %w[address null]
|
|
804
|
+
when Array, Range then %w[len empty map filter reject each fold sum sort uniq first last take drop join]
|
|
805
|
+
when String then %w[len empty upper lower trim split lines contains starts ends]
|
|
806
|
+
when Hash then %w[len empty keys values get has map filter each]
|
|
807
|
+
else []
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def native_method(receiver, name, args)
|
|
812
|
+
case receiver
|
|
813
|
+
when CommandValue
|
|
814
|
+
case name
|
|
815
|
+
when 'argv' then receiver.argv.dup
|
|
816
|
+
when 'result' then run_argv(receiver.argv)
|
|
817
|
+
when 'capture'
|
|
818
|
+
result = run_argv(receiver.argv)
|
|
819
|
+
@state.last_status = result['status']
|
|
820
|
+
result['out']
|
|
821
|
+
when 'check'
|
|
822
|
+
result = run_argv(receiver.argv)
|
|
823
|
+
@state.last_status = result['status']
|
|
824
|
+
if result['status'] != 0
|
|
825
|
+
detail = result['err'].to_s.strip
|
|
826
|
+
detail = detail.empty? ? '' : ": #{detail}"
|
|
827
|
+
raise RuntimeError, "command failed with status #{result['status']}: #{receiver.argv[0]}#{detail}"
|
|
828
|
+
end
|
|
829
|
+
result
|
|
830
|
+
when 'run'
|
|
831
|
+
raise RuntimeError, 'command.run() cannot own the terminal from a worker task; use .result()/.capture()' if @state.worker_thread?
|
|
832
|
+
ok = system(*receiver.argv)
|
|
833
|
+
status = $?.exitstatus || (ok ? 0 : 1)
|
|
834
|
+
@state.last_status = status
|
|
835
|
+
status
|
|
836
|
+
when 'task'
|
|
837
|
+
TaskValue.new { run_argv(receiver.argv) }
|
|
838
|
+
else raise RuntimeError, "unknown command method .#{name}"
|
|
839
|
+
end
|
|
840
|
+
when NamespaceValue
|
|
841
|
+
case name
|
|
842
|
+
when 'keys' then receiver.keys
|
|
843
|
+
when 'has' then receiver.member?(stringify(args[0]))
|
|
844
|
+
else raise RuntimeError, "unknown space method .#{name}"
|
|
845
|
+
end
|
|
846
|
+
when ObjectValue
|
|
847
|
+
case name
|
|
848
|
+
when 'fields' then receiver.fields
|
|
849
|
+
when 'methods' then @executor ? @executor.prototype_methods(receiver.proto_name) : []
|
|
850
|
+
when 'proto' then PrototypeRef.new(receiver.proto_name)
|
|
851
|
+
when 'clone' then receiver.copy
|
|
852
|
+
when 'is'
|
|
853
|
+
proto = args[0]
|
|
854
|
+
pname = proto.is_a?(PrototypeRef) ? proto.name : stringify(proto)
|
|
855
|
+
receiver.proto_name == pname
|
|
856
|
+
else raise RuntimeError, "unknown object method .#{name}"
|
|
857
|
+
end
|
|
858
|
+
when TaskValue
|
|
859
|
+
case name
|
|
860
|
+
when 'await' then receiver.await(args[0])
|
|
861
|
+
when 'done' then receiver.done?
|
|
862
|
+
when 'status' then receiver.status
|
|
863
|
+
when 'cancel' then receiver.cancel
|
|
864
|
+
else raise RuntimeError, "unknown task method .#{name}"
|
|
865
|
+
end
|
|
866
|
+
when ChannelValue
|
|
867
|
+
case name
|
|
868
|
+
when 'send' then receiver.send_value(args[0])
|
|
869
|
+
when 'recv' then receiver.recv(args[0])
|
|
870
|
+
when 'try_recv' then receiver.try_recv
|
|
871
|
+
when 'close' then receiver.close
|
|
872
|
+
when 'closed' then receiver.closed?
|
|
873
|
+
when 'size' then receiver.size
|
|
874
|
+
else raise RuntimeError, "unknown channel method .#{name}"
|
|
875
|
+
end
|
|
876
|
+
when AtomValue
|
|
877
|
+
case name
|
|
878
|
+
when 'get' then receiver.get
|
|
879
|
+
when 'set' then receiver.set(args[0])
|
|
880
|
+
when 'swap'
|
|
881
|
+
raise RuntimeError, 'atom.swap() expects a callable' unless args[0]
|
|
882
|
+
receiver.swap { |old| invoke_callable(args[0], [old]) }
|
|
883
|
+
else raise RuntimeError, "unknown atom method .#{name}"
|
|
884
|
+
end
|
|
885
|
+
when CBufferValue
|
|
886
|
+
case name
|
|
887
|
+
when 'size' then receiver.size
|
|
888
|
+
when 'address' then receiver.address
|
|
889
|
+
when 'ptr' then NativePointerValue.new(receiver.pointer)
|
|
890
|
+
when 'clear'
|
|
891
|
+
receiver.pointer[0, receiver.size] = "\0" * receiver.size
|
|
892
|
+
receiver
|
|
893
|
+
when 'read'
|
|
894
|
+
offset = args[0] ? numeric(args[0]).to_i : 0
|
|
895
|
+
length = args[1] ? numeric(args[1]).to_i : receiver.size - offset
|
|
896
|
+
raise RuntimeError, 'cbuf.read() range is outside buffer' if offset.negative? || length.negative? || offset + length > receiver.size
|
|
897
|
+
receiver.pointer[offset, length]
|
|
898
|
+
when 'string'
|
|
899
|
+
max = args[0] ? numeric(args[0]).to_i : receiver.size
|
|
900
|
+
raise RuntimeError, 'cbuf.string() length is outside buffer' if max.negative? || max > receiver.size
|
|
901
|
+
data = receiver.pointer[0, max]
|
|
902
|
+
data.split("\0", 2).first.to_s
|
|
903
|
+
when 'write'
|
|
904
|
+
data = stringify(args[0]).b
|
|
905
|
+
offset = args[1] ? numeric(args[1]).to_i : 0
|
|
906
|
+
raise RuntimeError, 'cbuf.write() range is outside buffer' if offset.negative? || offset + data.bytesize > receiver.size
|
|
907
|
+
receiver.pointer[offset, data.bytesize] = data
|
|
908
|
+
data.bytesize
|
|
909
|
+
else raise RuntimeError, "unknown cbuf method .#{name}"
|
|
910
|
+
end
|
|
911
|
+
when NativePointerValue
|
|
912
|
+
case name
|
|
913
|
+
when 'address' then receiver.address
|
|
914
|
+
when 'null' then receiver.null?
|
|
915
|
+
else raise RuntimeError, "unknown pointer method .#{name}"
|
|
916
|
+
end
|
|
917
|
+
when PrototypeRef
|
|
918
|
+
raise RuntimeError, "unknown prototype method .#{name}" unless name == 'new'
|
|
919
|
+
require_executor!('prototype construction')
|
|
920
|
+
@executor.instantiate(receiver.name, args)
|
|
921
|
+
when Array, Range
|
|
922
|
+
sequence_native_method(receiver, name, args)
|
|
923
|
+
when String
|
|
924
|
+
string_native_method(receiver, name, args)
|
|
925
|
+
when Hash
|
|
926
|
+
hash_native_method(receiver, name, args)
|
|
927
|
+
else
|
|
928
|
+
raise RuntimeError, "unknown method .#{name} for #{type_name(receiver)}"
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def sequence_native_method(receiver, name, args)
|
|
933
|
+
case name
|
|
934
|
+
when 'len' then receiver.length
|
|
935
|
+
when 'empty' then receiver.empty?
|
|
936
|
+
when 'map' then builtin_function('map', [receiver, args[0]])
|
|
937
|
+
when 'filter' then builtin_function('filter', [receiver, args[0]])
|
|
938
|
+
when 'reject' then builtin_function('reject', [receiver, args[0]])
|
|
939
|
+
when 'each' then builtin_function('each', [receiver, args[0]])
|
|
940
|
+
when 'fold' then builtin_function('fold', [receiver, args[0], args[1]])
|
|
941
|
+
when 'sum' then builtin_function('sum', [receiver, args[0]])
|
|
942
|
+
when 'sort' then builtin_function('sort', [receiver, args[0]])
|
|
943
|
+
when 'uniq' then sequence_values(receiver).uniq
|
|
944
|
+
when 'first' then sequence_values(receiver).first(args[0] ? numeric(args[0]).to_i : 1).then { |v| args[0] ? v : v.first }
|
|
945
|
+
when 'last' then args[0] ? sequence_values(receiver).last(numeric(args[0]).to_i) : sequence_values(receiver).last
|
|
946
|
+
when 'take' then builtin_function('take', [receiver, args[0]])
|
|
947
|
+
when 'drop' then builtin_function('drop', [receiver, args[0]])
|
|
948
|
+
when 'join' then sequence_values(receiver).join(stringify(args[0] || ''))
|
|
949
|
+
else raise RuntimeError, "unknown sequence method .#{name}"
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
def string_native_method(receiver, name, args)
|
|
954
|
+
case name
|
|
955
|
+
when 'len' then receiver.length
|
|
956
|
+
when 'empty' then receiver.empty?
|
|
957
|
+
when 'upper' then receiver.upcase
|
|
958
|
+
when 'lower' then receiver.downcase
|
|
959
|
+
when 'trim' then receiver.strip
|
|
960
|
+
when 'split' then receiver.split(args[0] ? stringify(args[0]) : nil)
|
|
961
|
+
when 'lines' then receiver.lines(chomp: true)
|
|
962
|
+
when 'contains' then receiver.include?(stringify(args[0]))
|
|
963
|
+
when 'starts' then receiver.start_with?(stringify(args[0]))
|
|
964
|
+
when 'ends' then receiver.end_with?(stringify(args[0]))
|
|
965
|
+
else raise RuntimeError, "unknown string method .#{name}"
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
def hash_native_method(receiver, name, args)
|
|
970
|
+
case name
|
|
971
|
+
when 'len' then receiver.length
|
|
972
|
+
when 'empty' then receiver.empty?
|
|
973
|
+
when 'keys' then receiver.keys
|
|
974
|
+
when 'values' then receiver.values
|
|
975
|
+
when 'get' then receiver.fetch(args[0], args[1])
|
|
976
|
+
when 'has' then receiver.key?(args[0])
|
|
977
|
+
when 'map' then builtin_function('map', [receiver, args[0]])
|
|
978
|
+
when 'filter' then builtin_function('filter', [receiver, args[0]])
|
|
979
|
+
when 'each' then builtin_function('each', [receiver, args[0]])
|
|
980
|
+
else raise RuntimeError, "unknown map method .#{name}"
|
|
981
|
+
end
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
def run_argv(argv)
|
|
985
|
+
max = 4 * 1024 * 1024
|
|
986
|
+
stdin = stdout = stderr = waiter = nil
|
|
987
|
+
buffers = {}
|
|
988
|
+
spawned = false
|
|
989
|
+
|
|
990
|
+
begin
|
|
991
|
+
stdin, stdout, stderr, waiter = Open3.popen3(*argv)
|
|
992
|
+
spawned = true
|
|
993
|
+
stdin.close
|
|
994
|
+
buffers = { stdout => +"", stderr => +"" }
|
|
995
|
+
open = buffers.keys.dup
|
|
996
|
+
|
|
997
|
+
until open.empty?
|
|
998
|
+
ready = IO.select(open, nil, nil, 0.1)&.first || []
|
|
999
|
+
ready.each do |io|
|
|
1000
|
+
begin
|
|
1001
|
+
chunk = io.read_nonblock(16 * 1024)
|
|
1002
|
+
buffer = buffers.fetch(io)
|
|
1003
|
+
if buffer.bytesize + chunk.bytesize > max
|
|
1004
|
+
raise RuntimeError, "command output exceeds #{max} bytes"
|
|
1005
|
+
end
|
|
1006
|
+
buffer << chunk
|
|
1007
|
+
rescue IO::WaitReadable
|
|
1008
|
+
rescue EOFError
|
|
1009
|
+
open.delete(io)
|
|
1010
|
+
io.close rescue nil
|
|
1011
|
+
end
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
status = waiter.value
|
|
1016
|
+
spawned = false
|
|
1017
|
+
{
|
|
1018
|
+
'out' => buffers.fetch(stdout, +""),
|
|
1019
|
+
'err' => buffers.fetch(stderr, +""),
|
|
1020
|
+
'status' => status.exitstatus || (status.signaled? ? 128 + status.termsig : 1)
|
|
1021
|
+
}
|
|
1022
|
+
ensure
|
|
1023
|
+
stdin.close rescue nil
|
|
1024
|
+
stdout.close rescue nil
|
|
1025
|
+
stderr.close rescue nil
|
|
1026
|
+
terminate_argv_process(waiter) if spawned && waiter
|
|
1027
|
+
end
|
|
1028
|
+
rescue Errno::ENOENT
|
|
1029
|
+
raise RuntimeError, "command not found: #{argv[0]}"
|
|
1030
|
+
rescue Errno::EACCES
|
|
1031
|
+
raise RuntimeError, "permission denied: #{argv[0]}"
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
def terminate_argv_process(waiter)
|
|
1035
|
+
return unless waiter
|
|
1036
|
+
pid = waiter.pid
|
|
1037
|
+
Process.kill('TERM', pid) rescue nil
|
|
1038
|
+
return waiter.value if waiter.join(0.25)
|
|
1039
|
+
|
|
1040
|
+
Process.kill('KILL', pid) rescue nil
|
|
1041
|
+
waiter.join(1.0)
|
|
1042
|
+
waiter.value rescue nil
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
def spawn_task(fn, args)
|
|
1046
|
+
worker_fn = worker_snapshot(fn)
|
|
1047
|
+
worker_args = worker_snapshot(args)
|
|
1048
|
+
TaskValue.new { concurrent_invoke(worker_fn, worker_args) }
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
def concurrent_invoke(fn, args)
|
|
1052
|
+
worker = self.class.new(@state, @executor)
|
|
1053
|
+
worker.send(:invoke_callable, fn, args)
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
def parallel_map(value, fn, workers_arg)
|
|
1057
|
+
items = sequence_values(value)
|
|
1058
|
+
return [] if items.empty?
|
|
1059
|
+
workers = workers_arg ? numeric(workers_arg).to_i : Etc.nprocessors
|
|
1060
|
+
workers = [[workers, 1].max, items.length, 64].min
|
|
1061
|
+
queue = Queue.new
|
|
1062
|
+
items.each_with_index { |item, index| queue << [index, item] }
|
|
1063
|
+
results = Array.new(items.length)
|
|
1064
|
+
errors = Queue.new
|
|
1065
|
+
|
|
1066
|
+
threads = Array.new(workers) do
|
|
1067
|
+
Thread.new do
|
|
1068
|
+
worker = self.class.new(@state, @executor)
|
|
1069
|
+
worker_fn = worker.worker_snapshot(fn)
|
|
1070
|
+
loop do
|
|
1071
|
+
pair = queue.pop(true) rescue nil
|
|
1072
|
+
break unless pair
|
|
1073
|
+
index, item = pair
|
|
1074
|
+
begin
|
|
1075
|
+
worker_item = worker.worker_snapshot(item)
|
|
1076
|
+
results[index] = worker.send(:invoke_callable, worker_fn, [worker_item])
|
|
1077
|
+
rescue StandardError => e
|
|
1078
|
+
errors << e
|
|
1079
|
+
break
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
threads.each(&:join)
|
|
1085
|
+
raise errors.pop unless errors.empty?
|
|
1086
|
+
results
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
def process_map(value, fn, workers_arg)
|
|
1090
|
+
items = sequence_values(value)
|
|
1091
|
+
return [] if items.empty?
|
|
1092
|
+
return parallel_map(value, fn, workers_arg) unless Process.respond_to?(:fork)
|
|
1093
|
+
|
|
1094
|
+
workers = workers_arg ? numeric(workers_arg).to_i : Etc.nprocessors
|
|
1095
|
+
workers = [[workers, 1].max, items.length, 32].min
|
|
1096
|
+
chunks = Array.new(workers) { [] }
|
|
1097
|
+
items.each_with_index { |item, index| chunks[index % workers] << [index, item] }
|
|
1098
|
+
children = []
|
|
1099
|
+
|
|
1100
|
+
chunks.each do |chunk|
|
|
1101
|
+
reader, writer = IO.pipe
|
|
1102
|
+
pid = fork do
|
|
1103
|
+
reader.close
|
|
1104
|
+
begin
|
|
1105
|
+
worker = self.class.new(@state, @executor)
|
|
1106
|
+
data = chunk.map do |index, item|
|
|
1107
|
+
[index, worker.send(:invoke_callable, fn, [item])]
|
|
1108
|
+
end
|
|
1109
|
+
Marshal.dump({ ok: true, data: data }, writer)
|
|
1110
|
+
rescue Exception => e
|
|
1111
|
+
begin
|
|
1112
|
+
Marshal.dump({ ok: false, error: "#{e.class}: #{e.message}" }, writer)
|
|
1113
|
+
rescue StandardError
|
|
1114
|
+
end
|
|
1115
|
+
ensure
|
|
1116
|
+
writer.close rescue nil
|
|
1117
|
+
exit!(0)
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
writer.close
|
|
1121
|
+
children << [pid, reader]
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
results = Array.new(items.length)
|
|
1125
|
+
errors = []
|
|
1126
|
+
children.each do |pid, reader|
|
|
1127
|
+
begin
|
|
1128
|
+
packet = Marshal.load(reader)
|
|
1129
|
+
if packet[:ok]
|
|
1130
|
+
packet[:data].each { |index, item| results[index] = item }
|
|
1131
|
+
else
|
|
1132
|
+
errors << packet[:error]
|
|
1133
|
+
end
|
|
1134
|
+
rescue EOFError, TypeError, ArgumentError => e
|
|
1135
|
+
errors << "worker #{pid}: #{e.class}: #{e.message}"
|
|
1136
|
+
ensure
|
|
1137
|
+
reader.close rescue nil
|
|
1138
|
+
Process.waitpid(pid) rescue nil
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
raise RuntimeError, "pmap worker failed: #{errors.first}" unless errors.empty?
|
|
1142
|
+
results
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def type_name(value)
|
|
1146
|
+
case value
|
|
1147
|
+
when nil then 'void'
|
|
1148
|
+
when Integer then 'int'
|
|
1149
|
+
when Float then 'float'
|
|
1150
|
+
when String then 'str'
|
|
1151
|
+
when Array then 'list'
|
|
1152
|
+
when Hash then 'map'
|
|
1153
|
+
when Range then 'range'
|
|
1154
|
+
when TrueClass, FalseClass then 'bool'
|
|
1155
|
+
when LambdaValue then 'lambda'
|
|
1156
|
+
when CodeValue then 'code'
|
|
1157
|
+
when CommandValue then 'command'
|
|
1158
|
+
when NativeLibraryValue then 'bridge'
|
|
1159
|
+
when NamespaceValue then 'space'
|
|
1160
|
+
when FunctionRef then 'function'
|
|
1161
|
+
when PrototypeRef then 'proto'
|
|
1162
|
+
when ObjectValue then value.proto_name
|
|
1163
|
+
when TaskValue then 'task'
|
|
1164
|
+
when ChannelValue then 'chan'
|
|
1165
|
+
when AtomValue then 'atom'
|
|
1166
|
+
when NativeFunctionValue then 'cfn'
|
|
1167
|
+
when NativePointerValue then 'ptr'
|
|
1168
|
+
when CBufferValue then 'cbuf'
|
|
1169
|
+
when BoundMethodValue, NativeMethodValue then 'method'
|
|
1170
|
+
else value.class.name
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
end
|
|
1174
|
+
end
|
|
1175
|
+
end
|