Module: R::Support

Defined in:
lib/R_interface/rsupport.rb,
lib/R_interface/r_methods.rb,
lib/R_interface/rsupport_scope.rb

Defined Under Namespace

Classes: BatchCollector

Constant Summary collapse

DISPATCH_PROBE_CACHE_MAX =

Phase 3: cache bridge dispatch_probe(handle, name) → { is_field, is_func } (FIFO eviction).

4096
TRANSPORT_NL =
"\uE000".freeze
MAX_UNBOX_DEPTH =

Maximum recursion depth when unboxing lists. Beyond this we raise UnboxDepthError.

100
MD_INDEX_BACKTICK =
'`[`'.freeze
MD_ASSIGN_BACKTICK =
'`[<-`'.freeze
@@var_id =
0
@@var_id_mutex =
Mutex.new

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.dispatch_probe_cache_hitsObject (readonly)

Returns the value of attribute dispatch_probe_cache_hits.



43
44
45
# File 'lib/R_interface/rsupport.rb', line 43

def dispatch_probe_cache_hits
  @dispatch_probe_cache_hits
end

.dispatch_probe_cache_missesObject (readonly)

Returns the value of attribute dispatch_probe_cache_misses.



43
44
45
# File 'lib/R_interface/rsupport.rb', line 43

def dispatch_probe_cache_misses
  @dispatch_probe_cache_misses
end

Class Method Details

.batch_eval_with_result(assignment_codes) ⇒ Object



670
671
672
# File 'lib/R_interface/rsupport.rb', line 670

def self.batch_eval_with_result(assignment_codes)
  R.bridge.batch_eval_r_with_result(Array(assignment_codes))
end

.build_subscript_assign_do_call_alist(receiver, kw_hash) ⇒ Object

[<- with i/j = :all (R::DataFrame#[]=). Same tidyselect / anyNA issues with missing_arg().



305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/R_interface/rsupport.rb', line 305

def self.build_subscript_assign_do_call_alist(receiver, kw_hash)
  pairs = kw_hash.map do |k, v|
    key = k.to_s.gsub(/__/, ".")
    key_r = (key =~ /\A[a-zA-Z._][a-zA-Z0-9._]*\z/) ? key : "`#{key.gsub('`', '\\`')}`"
    if v.is_a?(::Symbol) && v == :all
      "#{key_r} = "
    else
      "#{key_r} = #{parse_arg(v)}"
    end
  end
  inner = [parse_arg(receiver), *pairs].join(", ")
  "do.call(`[<-`, alist(#{inner}))"
end

.build_subscript_do_call_alist(all_args) ⇒ Object

R [ with a true missing dimension (Ruby :all). tidyselect on tbl_df rejects missing_arg() results as subscripts ("empty string"); alist(, ) is real missing.



296
297
298
299
300
301
302
# File 'lib/R_interface/rsupport.rb', line 296

def self.build_subscript_do_call_alist(all_args)
  inner = all_args.map do |arg|
    (arg.is_a?(::Symbol) && arg == :all) ? nil : parse_arg(arg)
  end
  inner = inner.map { |frag| frag.nil? ? '' : frag }.join(', ')
  "do.call(`[`, alist(#{inner}))"
end

.captureObject

Galaaz 2.0: In the Shadow Bridge, these methods return the name of the R function as a string handle.



31
32
33
# File 'lib/R_interface/r_methods.rb', line 31

def self.capture
  "capture" # Requires capture to be defined in R
end

.capture2Object



35
36
37
# File 'lib/R_interface/r_methods.rb', line 35

def self.capture2
  "capture2"
end

.clear_dispatch_probe_handle_cache!Object



57
58
59
60
61
62
# File 'lib/R_interface/rsupport.rb', line 57

def self.clear_dispatch_probe_handle_cache!
  @dispatch_probe_cache_mx.synchronize do
    @dispatch_probe_handle_cache.clear
    @dispatch_probe_handle_fifo.clear
  end
end

.convert_symbol2r(symbol) ⇒ Object

Convert a Ruby method name to the R name: __ => ., ___ => ::, rclass => class, eql => ==.



92
93
94
95
96
97
98
99
100
101
# File 'lib/R_interface/rsupport.rb', line 92

def self.convert_symbol2r(symbol)
  name = symbol.to_s
  name.gsub!(/___/, "::")
  name.gsub!(/__/, ".")
  case name
  when "rclass" then "class"
  when "eql"    then "=="
  else name
  end
end

.create_bin_expr(operator) ⇒ Object



67
68
69
# File 'lib/R_interface/r_methods.rb', line 67

def self.create_bin_expr(operator)
  "function(op1, op2) { #{operator}(op1, op2) }"
end

.dbk_indexObject



47
48
49
# File 'lib/R_interface/r_methods.rb', line 47

def self.dbk_index
  "`[[`"
end

.dispatch_probe_cache_key(handle, name) ⇒ Object



64
65
66
# File 'lib/R_interface/rsupport.rb', line 64

def self.dispatch_probe_cache_key(handle, name)
  "#{handle}\u0000#{name}"
end

.dispatch_probe_handle_cache_sizeObject



53
54
55
# File 'lib/R_interface/rsupport.rb', line 53

def self.dispatch_probe_handle_cache_size
  @dispatch_probe_cache_mx.synchronize { @dispatch_probe_handle_cache.size }
end

.enquoObject



59
60
61
# File 'lib/R_interface/r_methods.rb', line 59

def self.enquo
  "enquo"
end

.eval(string) ⇒ Object

Evaluate an R expression (string, Language, or handle). Returns a scalar value or R::Object.build(...). Uses .expression or .r_interop when present so we never treat R's printed output as code.



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
# File 'lib/R_interface/rsupport.rb', line 105

def self.eval(string)
  puts "DEBUG: Support.eval(#{string.inspect})" if ENV['GALAAZ_DEBUG']

  var_name = self.generate_var_name
  r_code = if string.respond_to?(:expression) && string.expression
             string.expression.to_s
           elsif string.respond_to?(:r_interop) && string.r_interop.is_a?(::String) && string.r_interop.start_with?("g2_v")
             string.r_interop
           else
             string.to_s
           end
  
  # Determine if we need to wrap in braces or use eval()
  # If it's a handle, we MUST use eval() in R to get its value if it's a symbol
  final_r_code = if r_code.start_with?("g2_v")
                   "eval(#{r_code})"
                 elsif r_code.include?("\n") && !r_code.strip.start_with?("{") && !r_code.include?("<-") && !r_code.include?("=")
                   "{#{r_code}\n}"
                 else
                   r_code
                 end

  assignment = "#{var_name} <- { #{final_r_code}\n }"
  envelope = R.bridge.eval_r_with_result(assignment)
  if ENV['GALAAZ_DEBUG']
    puts "DEBUG: eval envelope=#{envelope.inspect}"
  end
  raise "Result protocol: no envelope (buffer missing or invalid)" unless envelope

  case envelope[:type]
  when :scalar_double, :scalar_integer, :scalar_logical
    return envelope[:value]
  when :scalar_character
    return R::Object.build(var_name, nil, r_class: "character")
  when :scalar_symbol
    sym_name = envelope[:value].to_s
    return sym_name.gsub("::", "___").gsub(".", "__").to_sym
  when :handle
    # Protocol spec: eval returns scalar symbol as Ruby Symbol. R sends symbol/name as handle (type 4); unbox here only.
    r_class = envelope[:r_class].to_s.strip
    if r_class == "name" || r_class == "symbol"
      raw = R.bridge.eval_r("as.character(#{envelope[:handle]})").to_s
      m = raw.match(/\[1\]\s*"([^"]*)"/)
      name = m ? m[1] : raw.strip
      return name.gsub("::", "___").gsub(".", "__").to_sym
    end
    return R::Object.build(envelope[:handle], nil, r_class: envelope[:r_class], wrapper_tag: envelope[:wrapper_tag])
  else
    raise "Result protocol: unknown envelope type #{envelope[:type].inspect}"
  end
end

.exec_function(function, *args, unbox: false, **kwargs) ⇒ Object Also known as: exec_function_name

Run an R call: f_name(args...). Builds assignment, gets envelope from bridge, returns R::Object (boxed). Unbox with .to_ruby, .unboxed_get(0), or >> 0. kwargs (e.g. i:, value:) are merged into args for R named-argument calls like [[<-(df, i=..., value=...).



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
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/R_interface/rsupport.rb', line 324

def self.exec_function(function, *args, unbox: false, **kwargs)
  f_name = function.respond_to?(:r_interop) ? function.r_interop : function

  if args.empty? && kwargs.empty? && (f_name.include?("::") || f_name.start_with?("g2_v"))
    return self.eval(f_name)
  end

  var_name = self.generate_var_name
  all_args = kwargs.empty? ? args : args + [kwargs]

  use_subscript_alist =
    kwargs.empty? &&
    f_name == MD_INDEX_BACKTICK &&
    all_args.any? { |a| a.is_a?(::Symbol) && a == :all } &&
    !all_args.any? { |a| a.is_a?(Hash) }

  use_assign_alist =
    !kwargs.empty? &&
    f_name == MD_ASSIGN_BACKTICK &&
    all_args.size == 2 &&
    all_args[1].is_a?(Hash) &&
    all_args[1].values.any? { |v| v.is_a?(::Symbol) && v == :all }

  if use_subscript_alist
    r_expr = build_subscript_do_call_alist(all_args)
  elsif use_assign_alist
    r_expr = build_subscript_assign_do_call_alist(all_args[0], all_args[1])
  else
    r_args = all_args.map { |arg| self.parse_arg(arg) }
    # eval(expr, envir): R expects expr as expression; parse_arg on Language returns bare string -> wrap in parse(text=...) so envir is used
    if f_name == "eval" && args.size == 2 && !r_args[0].to_s.start_with?("g2_v", "quote(")
      r_args[0] = "parse(text=#{r_args[0].to_s.inspect})"
    end

    r_expr = "#{f_name}(#{r_args.join(", ")})"
  end
  assignment = "#{var_name} <- #{r_expr}"
  envelope = R.bridge.eval_r_with_result(assignment)
  unless envelope
    reason = R.bridge.respond_to?(:last_envelope_nil_reason) && R.bridge.last_envelope_nil_reason
    raise "Result protocol: no envelope (buffer missing or invalid)#{reason ? " [#{reason}]" : ''}"
  end

  ruby_result_from_envelope(envelope, var_name, r_expr)
end

.exec_function_async(function, *args, unbox: false, timeout: nil, **kwargs, &block) ⇒ Object

Async variant of exec_function; block receives NewBridge::EvalResult (+#value+ is like exec_function return).

Raises:

  • (ArgumentError)


391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/R_interface/rsupport.rb', line 391

def self.exec_function_async(function, *args, unbox: false, timeout: nil, **kwargs, &block)
  raise ArgumentError, 'exec_function_async requires a block' unless block

  f_name = function.respond_to?(:r_interop) ? function.r_interop : function

  if args.empty? && kwargs.empty? && (f_name.include?("::") || f_name.start_with?("g2_v"))
    raise ArgumentError, 'exec_function_async does not support bare handle/namespace reference; use R.eval_r_async'
  end

  var_name = self.generate_var_name
  all_args = kwargs.empty? ? args : args + [kwargs]

  use_subscript_alist =
    kwargs.empty? &&
    f_name == MD_INDEX_BACKTICK &&
    all_args.any? { |a| a.is_a?(::Symbol) && a == :all } &&
    !all_args.any? { |a| a.is_a?(Hash) }

  use_assign_alist =
    !kwargs.empty? &&
    f_name == MD_ASSIGN_BACKTICK &&
    all_args.size == 2 &&
    all_args[1].is_a?(Hash) &&
    all_args[1].values.any? { |v| v.is_a?(::Symbol) && v == :all }

  if use_subscript_alist
    r_expr = build_subscript_do_call_alist(all_args)
  elsif use_assign_alist
    r_expr = build_subscript_assign_do_call_alist(all_args[0], all_args[1])
  else
    r_args = all_args.map { |arg| self.parse_arg(arg) }
    if f_name == "eval" && args.size == 2 && !r_args[0].to_s.start_with?("g2_v", "quote(")
      r_args[0] = "parse(text=#{r_args[0].to_s.inspect})"
    end

    r_expr = "#{f_name}(#{r_args.join(", ")})"
  end
  assignment = "#{var_name} <- #{r_expr}"

  R.bridge.eval_r_with_result_async(assignment, timeout: timeout) do |result|
    if result.ok?
      begin
        env = result.value[:envelope]
        vn = result.value[:var_name]
        rx = result.value[:r_expr]
        obj = ruby_result_from_envelope(env, vn, rx)
        block.call(NewBridge::EvalResult.success(obj))
      rescue StandardError => e
        block.call(NewBridge::EvalResult.failure(e))
      end
    else
      block.call(result)
    end
  end
end

.expression_display_arg(arg) ⇒ Object

String suitable for expression display (to_s). For R::Language use stored expression; for other R::Object use R's deparse() so display reflects the current value (not the creating R code stored in .expression).



159
160
161
162
163
164
# File 'lib/R_interface/rsupport.rb', line 159

def self.expression_display_arg(arg)
  puts "DEBUG expression_display_arg: arg=#{arg.inspect} class=#{arg.class}" if ENV["GALAAZ_DEBUG"]
  return arg.expression if arg.is_a?(R::Language) && arg.respond_to?(:expression) && arg.expression
  return self.get_deparse_string(arg) if arg.is_a?(R::Object)
  self.parse_arg(arg).to_s
end

.generate_var_nameObject

Generate a unique R-side variable name (e.g. g2_v1, g2_v2) for assignment results.



84
85
86
87
88
89
# File 'lib/R_interface/rsupport.rb', line 84

def self.generate_var_name
  @@var_id_mutex.synchronize do
    @@var_id += 1
    "g2_v#{@@var_id}"
  end
end

.get_callback(id) ⇒ Object

Retrieve the Ruby proc registered for a callback id (from --G_CALLBACK--id--...).



646
647
648
# File 'lib/R_interface/rsupport.rb', line 646

def self.get_callback(id)
  @callbacks[id.to_i]
end

.get_deparse_string(arg) ⇒ Object

R-side deparse of an R::Object to a single string (e.g. "c(1L, 2L, 3L, 4L)"). Relies on R evaluating deparse() correctly on the object.



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/R_interface/rsupport.rb', line 168

def self.get_deparse_string(arg)
  puts "DEBUG get_deparse_string: called for #{arg.r_interop.inspect}" if ENV["GALAAZ_DEBUG"]
  dep = self.exec_function("deparse", arg)
  puts "DEBUG get_deparse_string: deparse result class=#{dep.class} value=#{dep.inspect}" if ENV["GALAAZ_DEBUG"]
  str = self.exec_function("paste0", dep, { collapse: "" })
  return str.to_s if str.is_a?(::String)
  raw = R.bridge.eval_r("paste0(#{str.r_interop}, collapse='')").to_s
  puts "DEBUG get_deparse_string: eval_r paste0 raw=#{raw.inspect}" if ENV["GALAAZ_DEBUG"]
  m = raw.match(/\[1\]\s*"([^"]*)"/)
  result = m ? m[1] : raw.strip
  puts "DEBUG get_deparse_string: returning #{result.inspect}" if ENV["GALAAZ_DEBUG"]
  result
end

.get_ruby_object(id_str) ⇒ Object

Retrieve the Ruby object for a handle "rb_obj_" returned from R.



651
652
653
654
# File 'lib/R_interface/rsupport.rb', line 651

def self.get_ruby_object(id_str)
  id = id_str.sub("rb_obj_", "").to_i
  @ruby_objects[id]
end

.md_indexObject



51
52
53
# File 'lib/R_interface/r_methods.rb', line 51

def self.md_index
  "`[`"
end

.new_scope(symbol, *args, &block) ⇒ Object



69
70
71
72
73
74
# File 'lib/R_interface/rsupport_scope.rb', line 69

def self.new_scope(symbol, *args, &block)
  executionScope = Scope.with(symbol, *args)
  scope = executionScope.new
  scope.instance_eval(&block)
  # scope
end

.parse_arg(arg) ⇒ Object

Turn a Ruby value into an R code fragment (string): handles, scalars, hashes -> list(), arrays -> c(), Procs -> R callback stub, etc.



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
# File 'lib/R_interface/rsupport.rb', line 183

def self.parse_arg(arg)
  return arg.r_interop if arg.respond_to?(:r_interop) && arg.r_interop
  return arg.expression if arg.respond_to?(:expression) && arg.expression

  case arg
  when Hash
    arg.map do |k, v|
      key = k.to_s.gsub(/__/, ".")
      # R list names with spaces or special chars must be backtick-quoted
      key_r = (key =~ /\A[a-zA-Z._][a-zA-Z0-9._]*\z/) ? key : "`#{key.gsub('`', '\\`')}`"
      "#{key_r} = #{self.parse_arg(v)}"
    end.join(", ")
  when Array
    "c(#{arg.map { |v| self.parse_arg(v) }.join(", ")})"
  when Symbol
    # :all means "all" in that dimension; R uses missing argument. Bridge defines missing_arg().
    return "missing_arg()" if arg == :all
    arg.to_s.gsub(/__/,".")
  when R::SymbolRef
    arg.to_r_symbol
  when String
    # If it's already a handle, don't quote it
    if arg.start_with?("g2_v")
      arg
    else
      "'#{arg.gsub("'", "\\\\'")}'"
    end
  when Numeric
    arg.to_s + (arg.is_a?(Integer) ? "L" : "")
  when TrueClass
    "TRUE"
  when FalseClass
    "FALSE"
  when Range
    final_value = (arg.exclude_end?) ? (arg.last - 1) : arg.last
    "seq(#{arg.first}, #{final_value})"
  when Proc, Method
    if R.bridge.respond_to?(:register_callback_proc_stub)
      return R.bridge.register_callback_proc_stub(arg)
    end

    id = R::Support.register_callback(arg)
    "function(...) {
      args <- list(...)
      handles <- character(0)
      if (length(args) > 0) {
        for (i in 1:length(args)) {
          h <- paste0('g2_v_cb_', i, '_', as.integer(runif(1, 1e8, 9e8)))
          assign(h, args[[i]], envir = .GlobalEnv)
          cls <- paste(class(args[[i]]), collapse=' ')
          handles <- c(handles, paste0(h, ':', cls))
        }
      }
      cat('--G_CALLBACK--#{id}--', paste(handles, collapse='|'), '--\\n', sep='')
      flush.console()
      while(TRUE) {
        res_str <- readLines('/dev/shm/galaaz_callback_fifo', n=1)
        if (length(res_str) < 1L) next
        line <- res_str[1]
        if (startsWith(line, '--G_CMD--')) {
          # Extract sequence number if present (regmatches returns list; use [[1]] for sub)
          seq_match <- regmatches(line, regexpr('--G_CMD--seq=([0-9]+)--', line))
          seq_num <- if (length(seq_match) > 0L && length(seq_match[[1]]) > 0L) sub('--G_CMD--seq=([0-9]+)--', '\\\\1', seq_match[[1]]) else '0'
          # Extract actual command
          cmd_part <- sub('--G_CMD--(seq=[0-9]+--)?', '', line)
          cmd <- trimws(cmd_part)
          # Restore newlines (Ruby sends U+E000 as placeholder so FIFO is one line)
          cmd <- gsub(\"#{TRANSPORT_NL}\", \"\\n\", cmd, fixed=TRUE)
          recv_log <- Sys.getenv('GALAAZ_R_RECEIVED_LOG', '')
          if (nchar(recv_log) > 0L) tryCatch({
            write('---CMD---\\n', file=recv_log, append=TRUE)
            write(cmd, file=recv_log, append=TRUE)
            write('\\n', file=recv_log, append=TRUE)
          }, error=function(e) NULL)
          # Log that we're about to execute a command
          cat('[R_CALLBACK]', 'seq=', seq_num, 'Executing:', substr(cmd, 1, 50), '...\\n', sep='')
          # Wrap entire command handling in tryCatch to ensure --G_CMD_END-- is always sent
          # Use capture.output with explicit print() to ensure output is captured
          tryCatch({
            if (nchar(cmd) > 0L) {
              # Evaluate in .GlobalEnv so g2_v* handles created by Ruby are visible.
              # Without this, eval() uses parent.frame() (chunk/engine env) and
              # object 'g2_vNNN' not found occurs when indexing vectors/matrices.
              result <- capture.output(print(eval(parse(text=cmd), envir = .GlobalEnv)))
              cat(result, sep='\\n')
            }
          }, error=function(e) {
            cat('--G_ERR--', conditionMessage(e), '\\n', sep='')
            tb <- paste(capture.output(traceback()), collapse = '\\\\\\\\n')
            if (nchar(tb) > 0L) cat('--G_TRACE--', tb, '\\n', sep='')
          }, finally={
            # Include sequence number in response for synchronization
            cat('--G_CMD_END--seq=', seq_num, '--\\n', sep='')
            flush.console()
          })
        } else if (startsWith(line, '--G_RET--')) {
          res_handle <- trimws(sub('--G_RET--', '', line))
          res_handle <- gsub(\"#{TRANSPORT_NL}\", \"\\n\", res_handle, fixed=TRUE)
          if (nchar(res_handle) > 0L) return(eval(parse(text=res_handle), envir = .GlobalEnv))
          return(invisible(NULL))
        }
      }
    }"
  when nil
    "NULL"
  else
    handle = self.register_ruby_object(arg)
    "'#{handle}'"
  end
end

.process_missing(symbol, internal, *args) ⇒ Object

Entry point for method_missing: handle setters (x=), eval, or dispatch to R (function / field / method with receiver).



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/R_interface/rsupport.rb', line 452

def self.process_missing(symbol, internal, *args)
  name = self.convert_symbol2r(symbol)
  if ENV['GALAAZ_DEBUG_EVAL'] && name == "expr" && args.size >= 1
    puts "[GALAAZ_DEBUG_EVAL] R.expr(...) called:"
    puts "  args[0].class = #{args[0].class}, args[0].inspect = #{args[0].inspect}"
  end
  return process_missing_setter(name, internal, args) if name =~ /(.*)=$/
  return process_missing_eval(name, internal, args) if name == "eval"
  # R.expr(expression_text): build R expression from string/SymbolExprString via parse(text=...), return R::Object.
  if name == "expr" && args.size == 1
    arg = args[0]
    if arg.is_a?(SymbolExprString) || (arg.is_a?(String) && !arg.start_with?("g2_v"))
      str = arg.to_s
      var_name = self.generate_var_name
      R.bridge.eval_r("#{var_name} <- parse(text=#{str.inspect})[[1]]")
      return R::Object.build(var_name)
    end
  end
  process_missing_dispatch(name, internal, args)
end

.process_missing_dispatch(name, internal, args) ⇒ Object

Dispatch: R module (eval handle/namespace or exec_function) or R::Object (length / function / field / fallback).



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
# File 'lib/R_interface/rsupport.rb', line 525

def self.process_missing_dispatch(name, internal, args)
  if internal == R && args.empty? && (name.include?("::") || name.start_with?("g2_v"))
    return R::Object.build(name)
  end

  # Phase 3: R.foo(...) — +internal+ is false/true from R.method_missing, not an R::Object. No per-handle
  # field vs function ambiguity; go straight to function execution.
  unless internal.is_a?(R::Object)
    return self.exec_function(name, *args)
  end

  handle = internal.r_interop
  return self.exec_function("length", internal, *args) if name == "length"

  # Prefer component/field access (obj.beta => obj[["beta"]]) over calling a global function (beta()).
  # Use [[ instead of $ so we avoid "$ operator is invalid for atomic vectors" when receiver is atomic;
  # [[ on list/data.frame/env returns the element; result protocol maps NA/NULL as appropriate.
  begin
    R.bridge.log_connections_in_r if ENV["GALAAZ_DEBUG_R"].to_s == "1" || ENV["GALAAZ_DEBUG_R"].to_s == "true"
    R.bridge.log_object_in_r(handle) if ENV["GALAAZ_DEBUG_R"].to_s == "1" || ENV["GALAAZ_DEBUG_R"].to_s == "true" || ENV["GALAAZ_DEBUG_OBJECT"].to_s == "1" || ENV["GALAAZ_DEBUG_OBJECT"].to_s == "true"
  rescue StandardError => e
    # Debug dump must not block the is_field check (e.g. if R throws "invalid connection" when inspecting the object).
    File.open(R.bridge.log_path("galaaz_obj_debug.log"), "a") { |f| f.puts "[#{Time.now.strftime('%H:%M:%S.%L')}] log_object_in_r(#{handle}) failed: #{e.message}" }
  end
  # In callback, eval_r can fail with "invalid connection" - wrap in begin/rescue
  is_field = false
  is_func = false
  if R.bridge.respond_to?(:dispatch_probe)
    # Environments are mutable (rm, assign); do not cache probe results — stale is_field breaks semantics.
    cache_probe = !internal.is_a?(::R::Environment)
    probe_key = dispatch_probe_cache_key(handle, name)
    cached_probe = nil
    if cache_probe
      @dispatch_probe_cache_mx.synchronize do
        cached_probe = @dispatch_probe_handle_cache[probe_key]
        @dispatch_probe_cache_hits += 1 if cached_probe
      end
    end
    if cached_probe
      is_field = !!cached_probe[:is_field]
      is_func = !!cached_probe[:is_func]
    else
      begin
        probe = R.bridge.dispatch_probe(handle, name)
        is_field = !!probe[:is_field]
        is_func = !!probe[:is_func]
        if cache_probe
          @dispatch_probe_cache_mx.synchronize do
            @dispatch_probe_cache_misses += 1
          end
          store_dispatch_probe_handle_cache(probe_key, is_field, is_func)
        end
        @dispatch_probe_cache[:func][name] = is_func
      rescue StandardError => e
        raise unless e.message.include?("invalid connection") || e.message.include?("invalid dispatch_probe params")
        is_field = false
        is_func = false
      end
    end
  else
    is_field = begin
      R.bridge.eval_r("isTRUE('#{name}' %in% names(#{handle})) || (is.environment(#{handle}) && isTRUE(exists('#{name}', envir = #{handle}, inherits = FALSE)))") == "[1] TRUE"
    rescue RuntimeError => e
      e.message.include?("invalid connection") ? false : raise
    end
    if is_field
      res = self.exec_function_name("`[[`", internal, name)
      return res.call(*args) if !args.empty? && res.respond_to?(:call)
      return res
    end

    is_func = begin
      cached = @dispatch_probe_cache[:func][name]
      if cached.nil?
        cached = (R.bridge.eval_r("is.function(try(get('#{name}'), silent=TRUE))") == "[1] TRUE")
        @dispatch_probe_cache[:func][name] = cached
      end
      cached
    rescue RuntimeError => e
      e.message.include?("invalid connection") ? false : raise
    end
  end
  if is_field
    res = self.exec_function_name("`[[`", internal, name)
    return res.call(*args) if !args.empty? && res.respond_to?(:call)
    return res
  end

  return self.exec_function(name, internal, *args) if is_func

  # Environment: missing name should raise NoMethodError (like Ruby), not call name(env) in R.
  if internal.is_a?(::R::Environment)
    ::Kernel.raise(::NoMethodError, "undefined method `#{name}' for #{internal.inspect}")
  end

  self.exec_function(name, internal, *args)
end

.process_missing_eval(name, internal, args) ⇒ Object

Handle obj.eval(env) or R.eval(code): expression in context, or single-arg R.eval.



492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'lib/R_interface/rsupport.rb', line 492

def self.process_missing_eval(name, internal, args)
  if internal.is_a?(R::Object) && name == "eval" && args.empty?
    return self.exec_function("eval", internal)
  end
  if internal.is_a?(R::Object) && args.size >= 1
    expr = self.parse_arg(internal)
    env = self.parse_arg(args[0])
    unless expr.start_with?("g2_v") || expr.start_with?("quote(")
      puts "DEBUG: Quoting expression: #{expr}" if ENV['GALAAZ_DEBUG']
      expr = "quote(#{expr})"
    end
    var_name = self.generate_var_name
    r_code = "#{var_name} <- eval(#{expr}, #{env})"
    if ENV['GALAAZ_DEBUG_EVAL']
      puts "[GALAAZ_DEBUG_EVAL] expr.eval(env) path:"
      puts "  internal.class = #{internal.class}"
      puts "  internal.r_interop = #{internal.r_interop.inspect}" if internal.respond_to?(:r_interop)
      puts "  internal.expression = #{internal.expression.inspect}" if internal.respond_to?(:expression)
      puts "  parse_arg(internal) => expr = #{expr.inspect}"
      puts "  parse_arg(args[0]) => env = #{env.inspect}"
      puts "  R code sent: #{r_code}"
    end
    R.bridge.eval_r(r_code)
    return R::Object.build(var_name)
  end
  if args.size == 1
    expr_arg = args[0].is_a?(SymbolExprString) ? args[0].to_s : args[0]
    return self.eval(expr_arg)
  end
  self.exec_function(name, *args)
end

.process_missing_setter(name, internal, args) ⇒ Object

Handle obj.var = rhs or R.var = rhs: use var<- or $<- on R::Object, else .GlobalEnv assignment.



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/R_interface/rsupport.rb', line 474

def self.process_missing_setter(name, internal, args)
  var = name[/^(.*)=$/, 1]
  rhs = self.parse_arg(args[0])
  if internal.is_a?(R::Object)
    handle = internal.r_interop
    r_var = (var == "rclass") ? "class" : var
    R.bridge.eval_r(<<~RCODE)
      #{handle} <- tryCatch(
        `#{r_var}<-`(#{handle}, #{rhs}),
        error = function(e) `$<-`(#{handle}, '#{var}', #{rhs})
      )
    RCODE
  else
    R.bridge.eval_r(".GlobalEnv$#{var} <- #{rhs}")
  end
end

.rangeObject



63
64
65
# File 'lib/R_interface/r_methods.rb', line 63

def self.range
  "range_helper"
end

.register_callback(proc) ⇒ Object

Register a Proc/Method to be invoked from R (e.g. in outer()); returns callback id for the R stub.



638
639
640
641
642
643
# File 'lib/R_interface/rsupport.rb', line 638

def self.register_callback(proc)
  @ruby_obj_id += 1
  id = @ruby_obj_id
  @callbacks[id] = proc
  id
end

.register_ruby_object(obj) ⇒ Object

Store a Ruby object for passing to R; returns handle string "rb_obj_" for use in R.



630
631
632
633
634
635
# File 'lib/R_interface/rsupport.rb', line 630

def self.register_ruby_object(obj)
  @ruby_object_id += 1
  id = @ruby_object_id
  @ruby_objects[id] = obj
  "rb_obj_#{id}"
end

.reset_dispatch_probe_cache_stats!Object



46
47
48
49
50
51
# File 'lib/R_interface/rsupport.rb', line 46

def self.reset_dispatch_probe_cache_stats!
  @dispatch_probe_cache_mx&.synchronize do
    @dispatch_probe_cache_hits = 0
    @dispatch_probe_cache_misses = 0
  end
end

.ruby_callback_methodObject



55
56
57
# File 'lib/R_interface/r_methods.rb', line 55

def self.ruby_callback_method
  "ruby_callback_method"
end

.ruby_result_from_envelope(envelope, var_name, r_expr) ⇒ Object

Build the same Ruby value as exec_function from a legacy envelope (+:handle+, scalars, rb_obj_*).



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/R_interface/rsupport.rb', line 371

def self.ruby_result_from_envelope(envelope, var_name, r_expr)
  case envelope[:type]
  when :scalar_double, :scalar_integer, :scalar_logical, :scalar_character
    val = envelope[:value]
    if envelope[:type] == :scalar_character && val.is_a?(String) && val =~ /^rb_obj_\d+$/
      return get_ruby_object(val)
    end
    r_class = { scalar_double: "numeric", scalar_integer: "integer", scalar_logical: "logical", scalar_character: "character" }[envelope[:type]]
    return R::Object.build(var_name, r_expr, r_class: r_class)
  when :scalar_symbol
    sym_name = envelope[:value].to_s
    return sym_name.gsub("::", "___").gsub(".", "__").to_sym
  when :handle
    return R::Object.build(envelope[:handle], r_expr, r_class: envelope[:r_class], wrapper_tag: envelope[:wrapper_tag])
  else
    raise "Result protocol: unknown envelope type #{envelope[:type].inspect}"
  end
end

.start_captureObject



39
40
41
# File 'lib/R_interface/r_methods.rb', line 39

def self.start_capture
  "start_capture"
end

.stop_captureObject



43
44
45
# File 'lib/R_interface/r_methods.rb', line 43

def self.stop_capture
  "stop_capture"
end

.store_dispatch_probe_handle_cache(key, is_field, is_func) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/R_interface/rsupport.rb', line 68

def self.store_dispatch_probe_handle_cache(key, is_field, is_func)
  @dispatch_probe_cache_mx.synchronize do
    if @dispatch_probe_handle_cache.key?(key)
      @dispatch_probe_handle_cache[key] = { is_field: is_field, is_func: is_func }
      return
    end
    while @dispatch_probe_handle_cache.size >= DISPATCH_PROBE_CACHE_MAX && !@dispatch_probe_handle_fifo.empty?
      oldest = @dispatch_probe_handle_fifo.shift
      @dispatch_probe_handle_cache.delete(oldest) if oldest
    end
    @dispatch_probe_handle_cache[key] = { is_field: is_field, is_func: is_func }
    @dispatch_probe_handle_fifo << key
  end
end