-
Notifications
You must be signed in to change notification settings - Fork 90
/
truffleruby.rb
355 lines (317 loc) · 10.5 KB
/
truffleruby.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# frozen_string_literal: true
module MiniRacer
class Context
class ExternalFunction
private
def notify_v8
name = @name.encode(::Encoding::UTF_8)
wrapped = lambda do |*args|
converted = @parent.send(:convert_js_to_ruby, args)
begin
result = @callback.call(*converted)
rescue Polyglot::ForeignException => e
e = RuntimeError.new(e.message)
e.set_backtrace(e.backtrace)
@parent.instance_variable_set(:@current_exception, e)
raise e
rescue => e
@parent.instance_variable_set(:@current_exception, e)
raise e
end
@parent.send(:convert_ruby_to_js, result)
end
if @parent_object.nil?
# set global name to proc
result = @parent.eval_in_context('this')
result[name] = wrapped
else
parent_object_eval = @parent_object_eval.encode(::Encoding::UTF_8)
begin
result = @parent.eval_in_context(parent_object_eval)
rescue Polyglot::ForeignException, StandardError => e
raise ParseError, "Was expecting #{@parent_object} to be an object", e.backtrace
end
result[name] = wrapped
# set evaluated object results name to proc
end
end
end
def heap_stats
{
total_physical_size: 0,
total_heap_size_executable: 0,
total_heap_size: 0,
used_heap_size: 0,
heap_size_limit: 0,
}
end
def stop
if @entered
@context.stop
@stopped = true
stop_attached
end
end
private
@context_initialized = false
@use_strict = false
def init_unsafe(isolate, snapshot)
unless defined?(Polyglot::InnerContext)
raise "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version"
end
unless Polyglot.languages.include? "js"
raise "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`\n" \
"You also need to install the 'js' component with 'gu install js' on GraalVM 22.2+\n" \
"Note that you need TruffleRuby+GraalVM and not just the TruffleRuby standalone to use MiniRacer"
end
@context = Polyglot::InnerContext.new(on_cancelled: -> {
raise ScriptTerminatedError, 'JavaScript was terminated (either by timeout or explicitly)'
})
Context.instance_variable_set(:@context_initialized, true)
@js_object = @context.eval('js', 'Object')
@isolate_mutex = Mutex.new
@stopped = false
@entered = false
@has_entered = false
@current_exception = nil
if isolate && snapshot
isolate.instance_variable_set(:@snapshot, snapshot)
end
if snapshot
@snapshot = snapshot
elsif isolate
@snapshot = isolate.instance_variable_get(:@snapshot)
else
@snapshot = nil
end
@is_object_or_array_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func = eval_in_context <<-CODE
[
(x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
(x) => { return x instanceof Date },
(x) => { return x.getTime(x) },
(x) => { return typeof x === 'symbol' },
(x) => { var r = x.description; return r === undefined ? 'undefined' : r },
(x) => { return new Date(x) },
(x) => { return new Array(x) },
]
CODE
end
def dispose_unsafe
@context.close
end
def eval_unsafe(str, filename)
@entered = true
if !@has_entered && @snapshot
snapshot_src = encode(@snapshot.instance_variable_get(:@source))
begin
eval_in_context(snapshot_src, filename)
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
end
@has_entered = true
raise RuntimeError, "TruffleRuby does not support eval after stop" if @stopped
raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
raise TypeError, "wrong type argument #{filename.class} (should be a string)" unless filename.nil? || filename.is_a?(String)
str = encode(str)
begin
translate do
eval_in_context(str, filename)
end
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
rescue ::RuntimeError => e
if @current_exception
e = @current_exception
@current_exception = nil
raise e
else
raise e, e.message
end
end
ensure
@entered = false
end
def call_unsafe(function_name, *arguments)
@entered = true
if !@has_entered && @snapshot
src = encode(@snapshot.instance_variable_get(:source))
begin
eval_in_context(src)
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
end
@has_entered = true
raise RuntimeError, "TruffleRuby does not support call after stop" if @stopped
begin
translate do
function = eval_in_context(function_name)
function.call(*convert_ruby_to_js(arguments))
end
rescue Polyglot::ForeignException => e
raise RuntimeError, e.message, e.backtrace
end
ensure
@entered = false
end
def create_isolate_value
# Returning a dummy object since TruffleRuby does not have a 1-1 concept with isolate.
# However, code and ASTs are shared between contexts.
Isolate.new
end
def isolate_mutex
@isolate_mutex
end
def translate
convert_js_to_ruby yield
rescue Object => e
message = e.message
if @current_exception
raise @current_exception
elsif e.message && e.message.start_with?('SyntaxError:')
error_class = MiniRacer::ParseError
elsif e.is_a?(MiniRacer::ScriptTerminatedError)
error_class = MiniRacer::ScriptTerminatedError
else
error_class = MiniRacer::RuntimeError
end
if error_class == MiniRacer::RuntimeError
bls = e.backtrace_locations&.select { |bl| bl&.source_location&.language == 'js' }
if bls && !bls.empty?
if '(eval)' != bls[0].path
message = "#{e.message}\n at #{bls[0]}\n" + bls[1..].map(&:to_s).join("\n")
else
message = "#{e.message}\n" + bls.map(&:to_s).join("\n")
end
end
raise error_class, message
else
raise error_class, message, e.backtrace
end
end
def convert_js_to_ruby(value)
case value
when true, false, Integer, Float
value
else
if value.nil?
nil
elsif value.respond_to?(:call)
MiniRacer::JavaScriptFunction.new
elsif value.respond_to?(:to_str)
value.to_str.dup
elsif value.respond_to?(:to_ary)
value.to_ary.map do |e|
if e.respond_to?(:call)
nil
else
convert_js_to_ruby(e)
end
end
elsif time?(value)
js_date_to_time(value)
elsif symbol?(value)
js_symbol_to_symbol(value)
else
object = value
h = {}
object.instance_variables.each do |member|
v = object[member]
unless v.respond_to?(:call)
h[member.to_s] = convert_js_to_ruby(v)
end
end
h
end
end
end
def object_or_array?(val)
@is_object_or_array_func.call(val)
end
def time?(value)
@is_time_func.call(value)
end
def js_date_to_time(value)
millis = @js_date_to_time_func.call(value)
Time.at(Rational(millis, 1000))
end
def symbol?(value)
@is_symbol_func.call(value)
end
def js_symbol_to_symbol(value)
@js_symbol_to_symbol_func.call(value).to_s.to_sym
end
def js_new_date(value)
@js_new_date_func.call(value)
end
def js_new_array(size)
@js_new_array_func.call(size)
end
def convert_ruby_to_js(value)
case value
when nil, true, false, Integer, Float
value
when Array
ary = js_new_array(value.size)
value.each_with_index do |v, i|
ary[i] = convert_ruby_to_js(v)
end
ary
when Hash
h = @js_object.new
value.each_pair do |k, v|
h[convert_ruby_to_js(k.to_s)] = convert_ruby_to_js(v)
end
h
when String, Symbol
Truffle::Interop.as_truffle_string value
when Time
js_new_date(value.to_f * 1000)
when DateTime
js_new_date(value.to_time.to_f * 1000)
else
"Undefined Conversion"
end
end
def encode(string)
raise ArgumentError unless string
string.encode(::Encoding::UTF_8)
end
class_eval <<-'RUBY', "(mini_racer)", 1
def eval_in_context(code, file = nil); code = ('"use strict";' + code) if Context.instance_variable_get(:@use_strict); @context.eval('js', code, file || '(mini_racer)'); end
RUBY
end
class Isolate
def init_with_snapshot(snapshot)
# TruffleRuby does not have a 1-1 concept with isolate.
# However, isolate can hold a snapshot, and code and ASTs are shared between contexts.
@snapshot = snapshot
end
def low_memory_notification
GC.start
end
def idle_notification(idle_time)
true
end
end
class Platform
def self.set_flag_as_str!(flag)
raise TypeError, "wrong type argument #{flag.class} (should be a string)" unless flag.is_a?(String)
raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.instance_variable_get(:@context_initialized)
Context.instance_variable_set(:@use_strict, true) if "--use_strict" == flag
end
end
class Snapshot
def load(str)
raise TypeError, "wrong type argument #{str.class} (should be a string)" unless str.is_a?(String)
# Intentionally noop since TruffleRuby mocks the snapshot API
end
def warmup_unsafe!(src)
raise TypeError, "wrong type argument #{src.class} (should be a string)" unless src.is_a?(String)
# Intentionally noop since TruffleRuby mocks the snapshot API
# by replaying snapshot source before the first eval/call
self
end
end
end