Module: Optimize::Codec::InstructionStream

Defined in:
lib/optimize/codec/instruction_stream.rb

Constant Summary collapse

U64_MASK =

Sign conversion for :OFFSET operands. IBF’s small_value primitive is unsigned; only branch OFFSETs are semantically signed (backward branches produce a negative relative slot offset). CRuby’s ibf_dump_small_value takes a VALUE (ulong), so a negative C long is implicitly reinterpreted as (2^64 + n); we do that explicitly.

(1 << 64) - 1
INT64_MIN =
-(1 << 63)
INT64_MAX =
(1 << 63) - 1
INSN_TABLE =

Maps opcode number -> [name_sym, operand_type_list]. Covers all non-trace, non-zjit opcodes for CRuby 4.0.2 (opcodes 0–108). Trace variants (109–217) and zjit variants (218–247) use the same operand shapes as their base opcodes and are added programmatically at the bottom.

{
  0   => [:nop,                          []],
  1   => [:getlocal,                     [:LINDEX, :NUM]],
  2   => [:setlocal,                     [:LINDEX, :NUM]],
  3   => [:getblockparam,                [:LINDEX, :NUM]],
  4   => [:setblockparam,                [:LINDEX, :NUM]],
  5   => [:getblockparamproxy,           [:LINDEX, :NUM]],
  6   => [:getspecial,                   [:NUM, :NUM]],
  7   => [:setspecial,                   [:NUM]],
  8   => [:getinstancevariable,          [:ID, :ISE]],
  9   => [:setinstancevariable,          [:ID, :ISE]],
  10  => [:getclassvariable,             [:ID, :IC]],
  11  => [:setclassvariable,             [:ID, :IC]],
  12  => [:opt_getconstant_path,         [:VALUE]],
  13  => [:getconstant,                  [:ID]],
  14  => [:setconstant,                  [:ID]],
  15  => [:getglobal,                    [:ID]],
  16  => [:setglobal,                    [:ID]],
  17  => [:putnil,                       []],
  18  => [:putself,                      []],
  19  => [:putobject,                    [:VALUE]],
  20  => [:putspecialobject,             [:NUM]],
  21  => [:putstring,                    [:VALUE]],
  22  => [:putchilledstring,             [:VALUE]],
  23  => [:concatstrings,                [:NUM]],
  24  => [:anytostring,                  []],
  25  => [:toregexp,                     [:NUM, :NUM]],
  26  => [:intern,                       []],
  27  => [:newarray,                     [:NUM]],
  28  => [:pushtoarraykwsplat,           []],
  29  => [:duparray,                     [:VALUE]],
  30  => [:duphash,                      [:VALUE]],
  31  => [:expandarray,                  [:NUM, :NUM]],
  32  => [:concatarray,                  []],
  33  => [:concattoarray,                []],
  34  => [:pushtoarray,                  []],
  35  => [:splatarray,                   [:NUM]],
  36  => [:splatkw,                      []],
  37  => [:newhash,                      [:NUM]],
  38  => [:newrange,                     [:NUM]],
  39  => [:pop,                          []],
  40  => [:dup,                          []],
  41  => [:dupn,                         [:NUM]],
  42  => [:swap,                         []],
  43  => [:opt_reverse,                  [:NUM]],
  44  => [:topn,                         [:NUM]],
  45  => [:setn,                         [:NUM]],
  46  => [:adjuststack,                  [:NUM]],
  47  => [:defined,                      [:NUM, :ID, :VALUE]],
  48  => [:definedivar,                  [:ID, :ISE]],
  49  => [:checkmatch,                   [:NUM]],
  50  => [:checkkeyword,                 [:NUM, :NUM]],
  51  => [:checktype,                    [:NUM]],
  52  => [:defineclass,                  [:ID, :ISEQ, :NUM]],
  53  => [:definemethod,                 [:ID, :ISEQ]],
  54  => [:definesmethod,                [:ID, :ISEQ]],
  55  => [:send,                         [:CALLDATA, :ISEQ]],
  56  => [:sendforward,                  [:CALLDATA, :ISEQ]],
  57  => [:opt_send_without_block,       [:CALLDATA]],
  58  => [:opt_new,                      [:CALLDATA, :ISEQ]],
  59  => [:objtostring,                  [:CALLDATA]],
  60  => [:opt_ary_freeze,               [:VALUE, :CALLDATA]],
  61  => [:opt_hash_freeze,              [:VALUE, :CALLDATA]],
  62  => [:opt_str_freeze,               [:VALUE, :CALLDATA]],
  63  => [:opt_nil_p,                    [:CALLDATA]],
  64  => [:opt_str_uminus,               [:VALUE, :CALLDATA]],
  65  => [:opt_duparray_send,            [:VALUE, :CALLDATA, :ISEQ]],
  66  => [:opt_newarray_send,            [:NUM, :CALLDATA]],
  67  => [:invokesuper,                  [:CALLDATA, :ISEQ, :NUM]],
  68  => [:invokesuperforward,           [:CALLDATA, :ISEQ, :NUM]],
  69  => [:invokeblock,                  [:CALLDATA, :NUM]],
  70  => [:leave,                        []],
  71  => [:throw,                        [:NUM]],
  72  => [:jump,                         [:OFFSET]],
  73  => [:branchif,                     [:OFFSET]],
  74  => [:branchunless,                 [:OFFSET]],
  75  => [:branchnil,                    [:OFFSET]],
  76  => [:once,                         [:ISEQ, :ISE]],
  77  => [:opt_case_dispatch,            [:CDHASH, :OFFSET]],
  78  => [:opt_plus,                     [:CALLDATA]],
  79  => [:opt_minus,                    [:CALLDATA]],
  80  => [:opt_mult,                     [:CALLDATA]],
  81  => [:opt_div,                      [:CALLDATA]],
  82  => [:opt_mod,                      [:CALLDATA]],
  83  => [:opt_eq,                       [:CALLDATA]],
  84  => [:opt_neq,                      [:CALLDATA, :CALLDATA]],
  85  => [:opt_lt,                       [:CALLDATA]],
  86  => [:opt_le,                       [:CALLDATA]],
  87  => [:opt_gt,                       [:CALLDATA]],
  88  => [:opt_ge,                       [:CALLDATA]],
  89  => [:opt_ltlt,                     [:CALLDATA]],
  90  => [:opt_and,                      [:CALLDATA]],
  91  => [:opt_or,                       [:CALLDATA]],
  92  => [:opt_aref,                     [:CALLDATA]],
  93  => [:opt_aset,                     [:CALLDATA, :CALLDATA]],
  94  => [:opt_length,                   [:CALLDATA]],
  95  => [:opt_size,                     [:CALLDATA]],
  96  => [:opt_empty_p,                  [:CALLDATA]],
  97  => [:opt_succ,                     [:CALLDATA]],
  98  => [:opt_not,                      [:CALLDATA]],
  99  => [:opt_regexpmatch2,             [:CALLDATA]],
  100 => [:invokebuiltin,                [:BUILTIN]],
  101 => [:opt_invokebuiltin_delegate,   [:BUILTIN, :NUM]],
  102 => [:opt_invokebuiltin_delegate_leave, [:BUILTIN, :NUM]],
  103 => [:getlocal_WC_0,               [:LINDEX]],
  104 => [:getlocal_WC_1,               [:LINDEX]],
  105 => [:setlocal_WC_0,               [:LINDEX]],
  106 => [:setlocal_WC_1,               [:LINDEX]],
  107 => [:putobject_INT2FIX_0_,        []],
  108 => [:putobject_INT2FIX_1_,        []],
}.freeze
BASE_OPCODE_COUNT =

Number of base (non-trace, non-zjit) opcodes.

109
OPCODE_TO_INFO =

Trace variants (109–217) have the same operand shapes as their base counterparts (0–108). zjit variants (218–247) also mirror base opcodes starting from 8 (getinstancevariable). We build a combined lookup hash for decode.

begin
  table = {}
  INSN_TABLE.each { |num, (name, ops)| table[num] = [name, ops] }

  # Trace variants: trace_X at num+109 mirrors base X at num (opcodes 0..108).
  INSN_TABLE.each do |base_num, (base_name, ops)|
    trace_num = base_num + BASE_OPCODE_COUNT
    trace_name = :"trace_#{base_name}"
    table[trace_num] = [trace_name, ops]
  end

  # zjit variants: zjit_X starts at 218 and mirrors a subset of base opcodes.
  # From RubyVM::INSTRUCTION_NAMES (Ruby 4.0.2), zjit starts at 218 with
  # zjit_getinstancevariable (base 8), zjit_setinstancevariable (9), etc.
  ZJIT_BASE_OPCODES = [8, 9, 48, 55, 57, 59, 63, 69, 78, 79, 80, 81, 82, 83, 84,
                       85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
  zjit_num = 218
  ZJIT_BASE_OPCODES.each do |base_num|
    base_name, ops = INSN_TABLE[base_num]
    next unless base_name
    zjit_name = :"zjit_#{base_name}"
    table[zjit_num] = [zjit_name, ops]
    zjit_num += 1
  end

  table.freeze
end
NAME_TO_OPCODE =

Maps name symbol -> opcode number (for encode).

OPCODE_TO_INFO.transform_values { |name, _ops| name }
.invert
.tap { |h| OPCODE_TO_INFO.each { |num, (name, _)| h[name] = num } }
.freeze

Class Method Summary collapse

Class Method Details

.decode(bytes, object_table, iseqs, ci_entries: []) ⇒ Array<IR::Instruction>

Decode bytes (a binary String) into an Array<IR::Instruction>.

Branch OFFSET operands in the binary are relative slot offsets: the number of YARV slots to skip from the NEXT instruction’s slot to reach the target. In IR, they are stored as absolute instruction indices. Convention: branch OFFSET operands are instruction indices in IR; they are relative YARV slot offsets (from next-insn) in the binary. See #encode for the reverse conversion.

Parameters:

  • bytes (String)

    raw bytecode bytes (ASCII-8BIT)

  • object_table (ObjectTable)

    decoded global object table (for index resolution)

  • iseqs (Array)

    iseq-list array (for TS_ISEQ resolution)

  • ci_entries (Array<IR::CallData>) (defaults to: [])

    decoded ci_entries records for this iseq, consumed in iteration order as CALLDATA operand slots are decoded.

Returns:



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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/optimize/codec/instruction_stream.rb', line 305

def self.decode(bytes, object_table, iseqs, ci_entries: [])
  ci = ci_entries.dup
  reader = BinaryReader.new(bytes)
  instructions = []
  # slot_to_insn_idx[slot] = instruction index; built as we decode.
  # slot is the YARV absolute slot number for the first slot of each instruction.
  slot_to_insn_idx = {}
  # Track each instruction's starting slot and op_types for OFFSET conversion.
  insn_slots = []  # [starting_slot, op_types] per instruction
  # Also track which instruction indices have OFFSET operands and which operand position.
  offset_operand_positions = [] # [[insn_idx, operand_idx], ...]
  current_slot = 0

  while reader.pos < bytes.bytesize
    opcode_offset = reader.pos
    opcode_num = reader.read_small_value

    info = OPCODE_TO_INFO[opcode_num]
    raise Codec::UnsupportedOpcode.new(opcode_num, opcode_offset) unless info

    _name, op_types = info
    opcode_sym = info[0]

    insn_idx = instructions.size
    slot_to_insn_idx[current_slot] = insn_idx
    insn_slot_start = current_slot
    insn_slots << [insn_slot_start, op_types]

    # map (not filter_map): every op_type contributes one operand slot so
    # operand indices line up with op_types. CALLDATA now carries the
    # IR::CallData record directly at its slot.
    operands = op_types.each_with_index.map do |op_type, op_idx|
      case op_type
      when :VALUE, :CDHASH, :ID, :ISEQ, :LINDEX, :NUM, :ISE, :IVC, :ICVARC, :IC
        reader.read_small_value
      when :OFFSET
        offset_operand_positions << [insn_idx, op_idx]
        u64_to_i64(reader.read_small_value)  # sign-extend: backward branches encode as (2^64 + n)
      when :CALLDATA
        # TS_CALLDATA: nothing in the bytecode stream, but we materialise
        # one IR::CallData record from the per-iseq ci_entries list.
        ci.shift or raise "InstructionStream.decode: ran out of ci_entries at insn #{opcode_sym}"
      when :BUILTIN
        # TS_BUILTIN: small_value index + small_value name_len + name bytes.
        idx = reader.read_small_value
        name_len = reader.read_small_value
        name_bytes = reader.read_bytes(name_len)
        [idx, name_len, name_bytes]
      else
        raise "Unknown operand type #{op_type.inspect} for opcode #{opcode_sym}"
      end
    end

    current_slot += slots_for(op_types)

    instructions << IR::Instruction.new(
      opcode:   opcode_sym,
      operands: operands,
      line:     nil,
    )
  end

  raise "InstructionStream.decode: #{ci.size} ci_entries unconsumed" unless ci.empty?

  # Convert OFFSET operands from YARV relative slot offsets to instruction indices.
  # The binary stores: OFFSET_raw = target_slot - next_insn_slot
  # where next_insn_slot = start_slot + slots_for(op_types) of the branching instruction.
  offset_operand_positions.each do |insn_idx, op_idx|
    raw_offset = instructions[insn_idx].operands[op_idx]
    start_slot, op_types = insn_slots[insn_idx]
    next_insn_slot = start_slot + slots_for(op_types)
    target_slot = next_insn_slot + raw_offset
    insn_target = slot_to_insn_idx[target_slot]
    raise "OFFSET raw=#{raw_offset} in #{instructions[insn_idx].opcode} targets slot #{target_slot} with no corresponding instruction" unless insn_target
    instructions[insn_idx].operands[op_idx] = insn_target
  end

  instructions
end

.encode(instructions, object_table, iseqs, ci_entries_out: []) ⇒ String

Encode instructions (Array<IR::Instruction>) back into a bytecode binary String.

Branch OFFSET operands in IR are absolute instruction indices; they are converted to relative YARV slot offsets (from next-insn) in the binary. Convention: branch OFFSET operands are instruction indices in IR; they are relative YARV slot offsets (from next-insn) in the binary. See #decode for the reverse conversion.

Parameters:

  • instructions (Array<IR::Instruction>)
  • object_table (ObjectTable)

    (unused in current identity encoding, reserved)

  • iseqs (Array)

    (unused in current identity encoding, reserved)

  • ci_entries_out (Array<IR::CallData>) (defaults to: [])

    mutable array that this method appends CALLDATA operands onto, in iteration order. The caller re-emits the iseq’s ci_entries section from this harvest.

Returns:

  • (String)

    ASCII-8BIT bytecode bytes



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
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/optimize/codec/instruction_stream.rb', line 400

def self.encode(instructions, object_table, iseqs, ci_entries_out: [])
  writer = BinaryWriter.new

  # Build instruction-index -> YARV starting slot map for OFFSET conversion.
  insn_to_slot = {}
  current_slot = 0
  instructions.each_with_index do |insn, idx|
    insn_to_slot[idx] = current_slot
    opcode_num = NAME_TO_OPCODE[insn.opcode]
    raise "Unknown opcode name: #{insn.opcode.inspect}" unless opcode_num
    _name, op_types = OPCODE_TO_INFO[opcode_num]
    current_slot += slots_for(op_types)
  end

  instructions.each_with_index do |insn, insn_idx|
    opcode_num = NAME_TO_OPCODE[insn.opcode]
    raise "Unknown opcode name: #{insn.opcode.inspect}" unless opcode_num

    writer.write_small_value(opcode_num)

    _name, op_types = OPCODE_TO_INFO[opcode_num]
    operand_idx = 0
    next_insn_slot = insn_to_slot[insn_idx] + slots_for(op_types)

    op_types.each do |op_type|
      case op_type
      when :VALUE, :CDHASH, :ID, :ISEQ, :LINDEX, :NUM, :ISE, :IVC, :ICVARC, :IC
        writer.write_small_value(insn.operands[operand_idx])
        operand_idx += 1
      when :OFFSET
        # Convert instruction index to YARV relative slot offset.
        # OFFSET_raw = target_slot - next_insn_slot, taken as a signed
        # i64 and re-interpreted as u64 (CRuby's implicit long<->VALUE
        # pun). Negative values always land in the 9-byte small_value form.
        target_insn_idx = insn.operands[operand_idx]
        target_slot = insn_to_slot[target_insn_idx]
        raise "OFFSET operand #{target_insn_idx} has no corresponding slot (out of range?)" unless target_slot
        writer.write_small_value(i64_to_u64(target_slot - next_insn_slot))
        operand_idx += 1
      when :CALLDATA
        # TS_CALLDATA: write nothing in the bytecode stream; harvest the
        # IR::CallData operand onto the caller-supplied output list so the
        # iseq's ci_entries section can be re-emitted from it.
        ci_entries_out << insn.operands[operand_idx]
        operand_idx += 1
      when :BUILTIN
        # TS_BUILTIN: stored as [idx, name_len, name_bytes] array.
        builtin = insn.operands[operand_idx]
        writer.write_small_value(builtin[0])
        writer.write_small_value(builtin[1])
        writer.write_bytes(builtin[2])
        operand_idx += 1
      else
        raise "Unknown operand type #{op_type.inspect} for opcode #{insn.opcode}"
      end
    end
  end

  writer.buffer
end

.i64_to_u64(i) ⇒ Object

Raises:

  • (ArgumentError)


25
26
27
28
# File 'lib/optimize/codec/instruction_stream.rb', line 25

def self.i64_to_u64(i)
  raise ArgumentError, "offset out of i64 range: #{i}" if i < INT64_MIN || i > INT64_MAX
  i & U64_MASK
end

.inst_to_slot_map(instructions) ⇒ Hash{IR::Instruction=>Integer}

Build an IR::Instruction → slot map (reverse of slot_map).

Parameters:

Returns:



277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/optimize/codec/instruction_stream.rb', line 277

def self.inst_to_slot_map(instructions)
  result = {}
  slot = 0
  instructions.each do |insn|
    opcode_num = NAME_TO_OPCODE[insn.opcode]
    raise "Unknown opcode name: #{insn.opcode.inspect}" unless opcode_num
    _name, op_types = OPCODE_TO_INFO[opcode_num]
    result[insn] = slot
    slot += slots_for(op_types)
  end
  result
end

.slot_map(instructions) ⇒ Hash{Integer=>IR::Instruction}

Build a slot → IR::Instruction map from a decoded instruction list. This reconstructs the mapping by walking instructions and computing slot sizes.

Parameters:

Returns:



240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/optimize/codec/instruction_stream.rb', line 240

def self.slot_map(instructions)
  map = {}
  slot = 0
  instructions.each do |insn|
    opcode_num = NAME_TO_OPCODE[insn.opcode]
    raise "Unknown opcode name: #{insn.opcode.inspect}" unless opcode_num
    _name, op_types = OPCODE_TO_INFO[opcode_num]
    map[slot] = insn
    slot += slots_for(op_types)
  end
  map
end

.slot_to_containing_inst_map(instructions) ⇒ Hash{Integer=>IR::Instruction}

Build a slot → IR::Instruction map that covers every YARV slot within each instruction’s range (not just the instruction start slot). This is used to decode insns_info entries that point to mid-instruction slots (“adjust” entries).

Parameters:

Returns:

  • (Hash{Integer=>IR::Instruction})

    YARV slot → instruction (any slot in range)



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/optimize/codec/instruction_stream.rb', line 259

def self.slot_to_containing_inst_map(instructions)
  map = {}
  slot = 0
  instructions.each do |insn|
    opcode_num = NAME_TO_OPCODE[insn.opcode]
    raise "Unknown opcode name: #{insn.opcode.inspect}" unless opcode_num
    _name, op_types = OPCODE_TO_INFO[opcode_num]
    size = slots_for(op_types)
    size.times { |i| map[slot + i] = insn }
    slot += size
  end
  map
end

.slots_for(op_types) ⇒ Object

Returns the number of YARV slots an instruction of the given op_types occupies (1 for the opcode itself + N for each operand slot). CALLDATA counts as 1 YARV slot even though it is not stored in the IBF binary. BUILTIN = 2 slots (idx + name_len; the name bytes are embedded and don’t count as YARV slots). Convention: branch OFFSET operands are stored as instruction indices in IR; they are YARV absolute slot indices in the binary.



212
213
214
215
216
217
218
219
# File 'lib/optimize/codec/instruction_stream.rb', line 212

def self.slots_for(op_types)
  1 + op_types.sum do |op_type|
    case op_type
    when :BUILTIN then 2
    else               1  # :CALLDATA, :OFFSET, :VALUE, :NUM, etc. all = 1 slot
    end
  end
end

.total_slots(instructions) ⇒ Integer

Compute the total number of YARV slots occupied by instructions. This is the value stored in the body record’s iseq_size field.

Parameters:

Returns:

  • (Integer)

    total YARV slot count



226
227
228
229
230
231
232
233
# File 'lib/optimize/codec/instruction_stream.rb', line 226

def self.total_slots(instructions)
  instructions.sum do |insn|
    opcode_num = NAME_TO_OPCODE[insn.opcode]
    raise "Unknown opcode name: #{insn.opcode.inspect}" unless opcode_num
    _name, op_types = OPCODE_TO_INFO[opcode_num]
    slots_for(op_types)
  end
end

.u64_to_i64(u) ⇒ Object



21
22
23
# File 'lib/optimize/codec/instruction_stream.rb', line 21

def self.u64_to_i64(u)
  u >= (1 << 63) ? u - (1 << 64) : u
end