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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +12 -3
  3. data/README.md +446 -8
  4. data/bin/srsh +71 -0
  5. data/docs/assets/slut.txt +4 -0
  6. data/docs/assets/srsh-mark.svg +12 -0
  7. data/docs/css/style.css +696 -0
  8. data/docs/index.html +703 -0
  9. data/docs/js/app.js +203 -0
  10. data/examples/bridge.rsh +8 -0
  11. data/examples/calculator.rsh +253 -0
  12. data/examples/defer.rsh +14 -0
  13. data/examples/hot.rsh +14 -0
  14. data/examples/meta.rsh +20 -0
  15. data/examples/modules/text.rsh +6 -0
  16. data/examples/modules.rsh +6 -0
  17. data/examples/paste.rsh +15 -0
  18. data/examples/plugin.rb +8 -0
  19. data/examples/power.rsh +65 -0
  20. data/examples/tour.rsh +38 -0
  21. data/ext/srsh_native/extconf.rb +3 -0
  22. data/ext/srsh_native/srsh_native.c +48 -0
  23. data/language-docs/LANGUAGE.md +670 -0
  24. data/language-docs/MIGRATION.md +44 -0
  25. data/language-docs/SECURITY.md +44 -0
  26. data/lib/srsh/app.rb +261 -0
  27. data/lib/srsh/builtins.rb +492 -0
  28. data/lib/srsh/editor.rb +530 -0
  29. data/lib/srsh/errors.rb +23 -0
  30. data/lib/srsh/history.rb +74 -0
  31. data/lib/srsh/language/evaluator.rb +1175 -0
  32. data/lib/srsh/language/lexer.rb +316 -0
  33. data/lib/srsh/language/parser.rb +997 -0
  34. data/lib/srsh/language/token.rb +5 -0
  35. data/lib/srsh/language/values.rb +392 -0
  36. data/lib/srsh/paths.rb +29 -0
  37. data/lib/srsh/plugins.rb +59 -0
  38. data/lib/srsh/process_identity.rb +38 -0
  39. data/lib/srsh/security.rb +38 -0
  40. data/lib/srsh/shell/executor.rb +1182 -0
  41. data/lib/srsh/shell/job.rb +101 -0
  42. data/lib/srsh/shell/lexer.rb +114 -0
  43. data/lib/srsh/shell/terminal.rb +26 -0
  44. data/lib/srsh/state.rb +136 -0
  45. data/lib/srsh/theme.rb +108 -0
  46. data/lib/srsh/version.rb +3 -0
  47. data/lib/srsh.rb +11 -5
  48. metadata +61 -14
  49. data/exe/srsh +0 -6
  50. 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