ruby-mana 0.4.0 → 0.5.1
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/CHANGELOG.md +48 -1
- data/README.md +68 -0
- data/data/lang-rules.yml +196 -0
- data/lib/mana/compiler.rb +1 -1
- data/lib/mana/engine.rb +38 -432
- data/lib/mana/engines/base.rb +79 -0
- data/lib/mana/engines/detect.rb +93 -0
- data/lib/mana/engines/javascript.rb +314 -0
- data/lib/mana/engines/llm.rb +467 -0
- data/lib/mana/engines/python.rb +314 -0
- data/lib/mana/engines/ruby_eval.rb +11 -0
- data/lib/mana/object_registry.rb +89 -0
- data/lib/mana/remote_ref.rb +85 -0
- data/lib/mana/version.rb +1 -1
- data/lib/mana.rb +11 -0
- metadata +25 -2
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mini_racer"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise LoadError, "mini_racer gem is required for JavaScript support. Add `gem 'mini_racer'` to your Gemfile."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require "json"
|
|
10
|
+
require "set"
|
|
11
|
+
|
|
12
|
+
module Mana
|
|
13
|
+
module Engines
|
|
14
|
+
class JavaScript < Base
|
|
15
|
+
# JS helper code that creates Proxy wrappers for remote Ruby objects.
|
|
16
|
+
# Injected once per V8 context.
|
|
17
|
+
JS_PROXY_HELPER = <<~JS
|
|
18
|
+
(function() {
|
|
19
|
+
if (typeof __mana_create_proxy !== 'undefined') return;
|
|
20
|
+
|
|
21
|
+
// FinalizationRegistry for automatic GC of remote refs.
|
|
22
|
+
// When a JS proxy is garbage collected, notify Ruby to release the original object.
|
|
23
|
+
if (typeof FinalizationRegistry !== 'undefined') {
|
|
24
|
+
globalThis.__mana_ref_gc = new FinalizationRegistry(function(refId) {
|
|
25
|
+
try { ruby.__ref_release(refId); } catch(e) {}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
globalThis.__mana_create_proxy = function(refId, typeName) {
|
|
30
|
+
var proxy = new Proxy({ __mana_ref: refId, __mana_type: typeName }, {
|
|
31
|
+
get: function(target, prop) {
|
|
32
|
+
if (prop === '__mana_ref') return target.__mana_ref;
|
|
33
|
+
if (prop === '__mana_type') return target.__mana_type;
|
|
34
|
+
if (prop === '__mana_alive') return ruby.__ref_alive(refId);
|
|
35
|
+
if (prop === 'release') return function() { ruby.__ref_release(refId); };
|
|
36
|
+
if (prop === 'toString' || prop === Symbol.toPrimitive) {
|
|
37
|
+
return function() { return ruby.__ref_to_s(refId); };
|
|
38
|
+
}
|
|
39
|
+
if (prop === 'inspect') {
|
|
40
|
+
return function() { return 'RemoteRef<' + typeName + '#' + refId + '>'; };
|
|
41
|
+
}
|
|
42
|
+
if (typeof prop === 'symbol') return undefined;
|
|
43
|
+
return function() {
|
|
44
|
+
var args = Array.prototype.slice.call(arguments);
|
|
45
|
+
return ruby.__ref_call(refId, prop, JSON.stringify(args));
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Register with FinalizationRegistry so GC triggers release
|
|
51
|
+
if (globalThis.__mana_ref_gc) {
|
|
52
|
+
globalThis.__mana_ref_gc.register(proxy, refId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return proxy;
|
|
56
|
+
};
|
|
57
|
+
})();
|
|
58
|
+
JS
|
|
59
|
+
|
|
60
|
+
# Thread-local persistent V8 context (lazy-loaded, long-running)
|
|
61
|
+
def self.context
|
|
62
|
+
Thread.current[:mana_js_context] ||= create_context
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.create_context
|
|
66
|
+
MiniRacer::Context.new
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.reset!
|
|
70
|
+
ctx = Thread.current[:mana_js_context]
|
|
71
|
+
ctx&.dispose
|
|
72
|
+
Thread.current[:mana_js_context] = nil
|
|
73
|
+
Thread.current[:mana_js_callbacks_attached] = nil
|
|
74
|
+
ObjectRegistry.reset!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def execute(code)
|
|
78
|
+
ctx = self.class.context
|
|
79
|
+
|
|
80
|
+
# 1. Attach Ruby callbacks (methods + effects + ref operations)
|
|
81
|
+
attach_ruby_callbacks(ctx)
|
|
82
|
+
|
|
83
|
+
# 2. Inject Ruby variables into JS scope
|
|
84
|
+
inject_ruby_vars(ctx, code)
|
|
85
|
+
|
|
86
|
+
# 3. Execute the JS code
|
|
87
|
+
result = ctx.eval(code)
|
|
88
|
+
|
|
89
|
+
# 4. Extract any new/modified variables back to Ruby binding
|
|
90
|
+
extract_js_vars(ctx, code)
|
|
91
|
+
|
|
92
|
+
result
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def attach_ruby_callbacks(ctx)
|
|
98
|
+
attached = Thread.current[:mana_js_callbacks_attached] ||= Set.new
|
|
99
|
+
|
|
100
|
+
# Install the JS Proxy helper (once per context)
|
|
101
|
+
unless attached.include?("__proxy_helper")
|
|
102
|
+
ctx.eval(JS_PROXY_HELPER)
|
|
103
|
+
attached << "__proxy_helper"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ruby.read / ruby.write -- variable bridge
|
|
107
|
+
unless attached.include?("ruby.read")
|
|
108
|
+
bnd = @binding
|
|
109
|
+
ctx.attach("ruby.read", proc { |name|
|
|
110
|
+
sym = name.to_sym
|
|
111
|
+
if bnd.local_variables.include?(sym)
|
|
112
|
+
val = bnd.local_variable_get(sym)
|
|
113
|
+
json_safe(val)
|
|
114
|
+
else
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
})
|
|
118
|
+
attached << "ruby.read"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
unless attached.include?("ruby.write")
|
|
122
|
+
bnd = @binding
|
|
123
|
+
ctx.attach("ruby.write", proc { |name, value|
|
|
124
|
+
bnd.local_variable_set(name.to_sym, value)
|
|
125
|
+
value
|
|
126
|
+
})
|
|
127
|
+
attached << "ruby.write"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Remote reference operations
|
|
131
|
+
attach_ref_callbacks(ctx, attached)
|
|
132
|
+
|
|
133
|
+
# Attach methods from the caller's receiver
|
|
134
|
+
attach_receiver_methods(ctx, attached)
|
|
135
|
+
|
|
136
|
+
# Attach registered Mana effects
|
|
137
|
+
attach_effects(ctx, attached)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Attach callbacks for operating on remote Ruby object references from JS.
|
|
141
|
+
def attach_ref_callbacks(ctx, attached)
|
|
142
|
+
registry = ObjectRegistry.current
|
|
143
|
+
|
|
144
|
+
unless attached.include?("ruby.__ref_call")
|
|
145
|
+
ctx.attach("ruby.__ref_call", proc { |ref_id, method_name, args_json|
|
|
146
|
+
obj = registry.get(ref_id)
|
|
147
|
+
raise "Remote reference #{ref_id} has been released" unless obj
|
|
148
|
+
|
|
149
|
+
args = args_json ? JSON.parse(args_json) : []
|
|
150
|
+
result = obj.public_send(method_name.to_sym, *args)
|
|
151
|
+
json_safe(result)
|
|
152
|
+
})
|
|
153
|
+
attached << "ruby.__ref_call"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
unless attached.include?("ruby.__ref_release")
|
|
157
|
+
ctx.attach("ruby.__ref_release", proc { |ref_id|
|
|
158
|
+
registry.release(ref_id)
|
|
159
|
+
nil
|
|
160
|
+
})
|
|
161
|
+
attached << "ruby.__ref_release"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
unless attached.include?("ruby.__ref_alive")
|
|
165
|
+
ctx.attach("ruby.__ref_alive", proc { |ref_id|
|
|
166
|
+
registry.registered?(ref_id)
|
|
167
|
+
})
|
|
168
|
+
attached << "ruby.__ref_alive"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
unless attached.include?("ruby.__ref_to_s")
|
|
172
|
+
ctx.attach("ruby.__ref_to_s", proc { |ref_id|
|
|
173
|
+
obj = registry.get(ref_id)
|
|
174
|
+
obj ? obj.to_s : "released"
|
|
175
|
+
})
|
|
176
|
+
attached << "ruby.__ref_to_s"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def attach_receiver_methods(ctx, attached)
|
|
181
|
+
receiver = @binding.receiver
|
|
182
|
+
user_methods = receiver.class.instance_methods(false) -
|
|
183
|
+
Object.instance_methods -
|
|
184
|
+
[:~@]
|
|
185
|
+
|
|
186
|
+
user_methods.each do |method_name|
|
|
187
|
+
key = "ruby.#{method_name}"
|
|
188
|
+
next if attached.include?(key)
|
|
189
|
+
|
|
190
|
+
recv = receiver
|
|
191
|
+
ctx.attach(key, proc { |*args| json_safe(recv.public_send(method_name, *args)) })
|
|
192
|
+
attached << key
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def attach_effects(ctx, attached)
|
|
197
|
+
Mana::EffectRegistry.registry.each do |name, effect|
|
|
198
|
+
key = "ruby.#{name}"
|
|
199
|
+
next if attached.include?(key)
|
|
200
|
+
|
|
201
|
+
eff = effect
|
|
202
|
+
ctx.attach(key, proc { |*args|
|
|
203
|
+
input = if args.length == 1 && args[0].is_a?(Hash)
|
|
204
|
+
args[0]
|
|
205
|
+
elsif eff.params.length == args.length
|
|
206
|
+
eff.params.zip(args).to_h { |p, v| [p[:name], v] }
|
|
207
|
+
else
|
|
208
|
+
{}
|
|
209
|
+
end
|
|
210
|
+
json_safe(eff.call(input))
|
|
211
|
+
})
|
|
212
|
+
attached << key
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def inject_ruby_vars(ctx, code)
|
|
217
|
+
@binding.local_variables.each do |var_name|
|
|
218
|
+
next unless code.match?(/\b#{Regexp.escape(var_name.to_s)}\b/)
|
|
219
|
+
|
|
220
|
+
value = @binding.local_variable_get(var_name)
|
|
221
|
+
inject_value(ctx, var_name.to_s, value)
|
|
222
|
+
rescue => e
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Inject a single value into the JS context.
|
|
228
|
+
# Simple types are serialized to JSON. Complex objects become remote ref proxies.
|
|
229
|
+
def inject_value(ctx, name, value)
|
|
230
|
+
case value
|
|
231
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
|
232
|
+
ctx.eval("var #{name} = #{JSON.generate(value)}")
|
|
233
|
+
when Symbol
|
|
234
|
+
ctx.eval("var #{name} = #{JSON.generate(value.to_s)}")
|
|
235
|
+
when Array
|
|
236
|
+
ctx.eval("var #{name} = #{JSON.generate(serialize_array(value))}")
|
|
237
|
+
when Hash
|
|
238
|
+
ctx.eval("var #{name} = #{JSON.generate(serialize_hash(value))}")
|
|
239
|
+
else
|
|
240
|
+
# Complex object → register and create JS Proxy
|
|
241
|
+
ref_id = ObjectRegistry.current.register(value)
|
|
242
|
+
ctx.eval("var #{name} = __mana_create_proxy(#{ref_id}, #{JSON.generate(value.class.name)})")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def serialize_array(arr)
|
|
247
|
+
arr.map { |v| simple_value?(v) ? serialize_simple(v) : v.to_s }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def serialize_hash(hash)
|
|
251
|
+
hash.transform_keys(&:to_s).transform_values { |v|
|
|
252
|
+
simple_value?(v) ? serialize_simple(v) : v.to_s
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def simple_value?(value)
|
|
257
|
+
case value
|
|
258
|
+
when Numeric, String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash
|
|
259
|
+
true
|
|
260
|
+
else
|
|
261
|
+
false
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def serialize_simple(value)
|
|
266
|
+
case value
|
|
267
|
+
when Symbol then value.to_s
|
|
268
|
+
when Array then serialize_array(value)
|
|
269
|
+
when Hash then serialize_hash(value)
|
|
270
|
+
else value
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def extract_js_vars(ctx, code)
|
|
275
|
+
declared_vars = extract_declared_vars(code)
|
|
276
|
+
declared_vars.each do |var_name|
|
|
277
|
+
begin
|
|
278
|
+
value = ctx.eval(var_name)
|
|
279
|
+
deserialized = deserialize(value)
|
|
280
|
+
write_var(var_name, deserialized)
|
|
281
|
+
rescue MiniRacer::RuntimeError
|
|
282
|
+
next
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def extract_declared_vars(code)
|
|
288
|
+
vars = []
|
|
289
|
+
code.scan(/\b(?:const|let|var)\s+(\w+)\s*=/).each { |m| vars << m[0] }
|
|
290
|
+
code.scan(/^(\w+)\s*=[^=>]/).each { |m| vars << m[0] }
|
|
291
|
+
vars.uniq
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def deserialize(value)
|
|
295
|
+
value
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def json_safe(value)
|
|
299
|
+
case value
|
|
300
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
|
301
|
+
value
|
|
302
|
+
when Symbol
|
|
303
|
+
value.to_s
|
|
304
|
+
when Array
|
|
305
|
+
value.map { |v| json_safe(v) }
|
|
306
|
+
when Hash
|
|
307
|
+
value.transform_keys(&:to_s).transform_values { |v| json_safe(v) }
|
|
308
|
+
else
|
|
309
|
+
value.to_s
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|