Class: Optimize::Codec::IseqList
- Inherits:
-
Object
- Object
- Optimize::Codec::IseqList
- Defined in:
- lib/optimize/codec/iseq_list.rb
Overview
Decodes and encodes the iseq list section of a YARB binary.
Binary layout (from research/cruby/ibf-format.md §2 and §4):
[40] Iseq data region — data sections and body records for all iseqs
(interleaved; each iseq written by ibf_dump_iseq_each; body record last)
[X] Iseq offset array — iseq_list_size × uint32_t at header.iseq_list_offset
Each entry is the absolute offset of that iseq's body record.
The ObjectTable follows immediately after the iseq offset array.
Decode strategy:
1. Read the iseq offset array (random-access via header.iseq_list_offset).
2. Capture the raw iseq data region (bytes 40 .. iseq_list_offset-1 plus the
offset array itself) for verbatim re-emission.
3. Decode each body record to build IR::Function objects.
4. Wire up parent/child relationships using parent_iseq_index.
Encode strategy:
1. Write the raw iseq data region verbatim.
2. Write the iseq offset array (the body record offsets stay the same since we
re-emit the data region byte-for-byte).
Constant Summary collapse
- ISEQ_REGION_START =
Absolute byte offset where the iseq data region begins in the binary.
40
Instance Attribute Summary collapse
-
#functions ⇒ Array<IR::Function>
readonly
All functions in iseq-list order.
-
#root ⇒ IR::Function
readonly
The root (top-level) iseq — typically functions.
Class Method Summary collapse
-
.decode(binary, header, object_table) ⇒ IseqList
Decode the iseq list from
binaryusingheaderandobject_table.
Instance Method Summary collapse
-
#encode(writer) ⇒ Object
Encode the iseq list into
writer. -
#initialize(functions, root, raw_offset_array, object_table, raw_trailing = "".b) ⇒ IseqList
constructor
A new instance of IseqList.
Constructor Details
#initialize(functions, root, raw_offset_array, object_table, raw_trailing = "".b) ⇒ IseqList
Returns a new instance of IseqList.
48 49 50 51 52 53 54 |
# File 'lib/optimize/codec/iseq_list.rb', line 48 def initialize(functions, root, raw_offset_array, object_table, raw_trailing = "".b) @functions = functions @root = root @raw_offset_array = raw_offset_array # iseq_list_size × 4 bytes @object_table = object_table @raw_trailing = raw_trailing # trailing bytes after last body record (before list_offset) end |
Instance Attribute Details
#functions ⇒ Array<IR::Function> (readonly)
Returns all functions in iseq-list order.
40 41 42 |
# File 'lib/optimize/codec/iseq_list.rb', line 40 def functions @functions end |
#root ⇒ IR::Function (readonly)
Returns the root (top-level) iseq — typically functions.
43 44 45 |
# File 'lib/optimize/codec/iseq_list.rb', line 43 def root @root end |
Class Method Details
.decode(binary, header, object_table) ⇒ IseqList
Decode the iseq list from binary using header and object_table.
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 |
# File 'lib/optimize/codec/iseq_list.rb', line 62 def self.decode(binary, header, object_table) iseq_count = header.iseq_list_size list_offset = header.iseq_list_offset # Capture raw offset array bytes. raw_offset_array = binary.byteslice(list_offset, iseq_count * 4) # Read the offset array. body_offsets = raw_offset_array.unpack("V*") # V = uint32 little-endian # First pass: decode each body record, building IR::Function stubs. # all_functions[i] corresponds to iseq-list index i. all_functions = Array.new(iseq_count) body_offsets.each_with_index do |body_offset, idx| all_functions[idx] = IseqEnvelope.decode( binary, body_offset, header, object_table, all_functions ) end # Capture per-function raw slices: per-section alignment padding and raw content. # # Section order (from ibf-format.md §4.1): # bytecode → opt_table → kw → insns_info_body → insns_info_positions → # local_table → lvar_states → catch_table → ci_entries → outer_vars # # For each section we capture: # misc[:"#{key}_pad_raw"] — alignment bytes immediately BEFORE this section's abs offset. # Computed as binary[prev_content_end .. abs-1]. # misc[:"#{key}_raw"] — section content bytes. # For bytecode: exact bytecode_size bytes (no trailing pad). # For all other sections: bytes from abs to next_section_abs-1, # which INCLUDES any trailing alignment padding before the # next section. At encode time, this trailing pad is reproduced # by writing IR_content + (raw.bytesize - ir_content.bytesize) # zero bytes, which is byte-identical since CRuby pads with zeros. # # After emit_section for non-bytecode sections, content_end advances by the FULL raw # size (abs + raw.bytesize), so the next section's pad_len calculation is 0 (the pad # is already embedded at the tail of the current section's raw blob). prev_end = ISEQ_REGION_START # byte offset where previous function's data ended body_offsets.each_with_index do |body_offset, idx| fn = all_functions[idx] misc = fn.misc raw_body = misc[:raw_body] # Build the ordered list of (section_key, abs_offset) for this function, # sorted by ascending abs offset. Absent sections (abs nil or 0) are excluded. section_order = [ [:bytecode, misc[:bytecode_abs]], [:opt_table, misc[:opt_table_abs]], [:kw, misc[:kw_abs]], [:insns_body, misc[:insns_body_abs]], [:insns_pos, misc[:insns_pos_abs]], [:local_table, misc[:local_table_abs]], [:lvar_states, misc[:lvar_states_abs]], [:catch_table, misc[:catch_table_abs]], [:ci_entries, misc[:ci_entries_abs]], [:outer_vars, misc[:outer_vars_abs]], ].select { |_, abs| abs && abs > 0 } .sort_by { |_, abs| abs } # Walk the sections, capturing pad and raw content. # content_end tracks where the previous section's content (+ embedded trailing pad) ended. content_end = prev_end section_order.each_with_index do |(key, abs), i| # Alignment padding: bytes from content_end up to (but not including) this section's abs. pad_len = abs - content_end misc[:"#{key}_pad_raw"] = pad_len > 0 ? binary.byteslice(content_end, pad_len) : "".b # Section content size. # Bytecode: exact size from body record (no trailing pad). # Others: span from abs to just before the next section's abs (or body_offset if last). # This span includes any trailing alignment zeros before the next section. if key == :bytecode content_size = misc[:bytecode_size] || 0 else next_abs = (i + 1 < section_order.size) ? section_order[i + 1][1] : body_offset content_size = next_abs - abs end raw_bytes = content_size > 0 ? binary.byteslice(abs, content_size) : "".b misc[:"#{key}_raw"] = raw_bytes # Advance content_end by the full raw size (so next section's pad_len = 0 # if trailing pad is embedded in our raw). content_end = abs + content_size end # Also set pre_bytecode_raw / post_bytecode_raw for backward compat. bytecode_abs = misc[:bytecode_abs] bytecode_size = misc[:bytecode_size] if bytecode_abs && bytecode_size && bytecode_size > 0 pre_len = bytecode_abs - prev_end pre_bytes = pre_len > 0 ? binary.byteslice(prev_end, pre_len) : "".b post_start = bytecode_abs + bytecode_size post_len = body_offset - post_start post_bytes = post_len > 0 ? binary.byteslice(post_start, post_len) : "".b misc[:pre_bytecode_raw] = pre_bytes misc[:post_bytecode_raw] = post_bytes else pre_len = body_offset - prev_end pre_bytes = pre_len > 0 ? binary.byteslice(prev_end, pre_len) : "".b misc[:pre_bytecode_raw] = pre_bytes misc[:post_bytecode_raw] = "".b end prev_end = body_offset + raw_body.bytesize end # Capture any trailing bytes in the iseq data region after the last body record. # These bytes (if any) exist between the last body record's end and list_offset. trailing_len = list_offset - prev_end raw_trailing = trailing_len > 0 ? binary.byteslice(prev_end, trailing_len) : "".b # Second pass: wire up parent/child relationships. # Each function's misc[:parent_iseq_index] tells us who its parent is. # The sentinel for "no parent" is -1 stored as a huge unsigned int in small_value # (CRuby uses ibf_offset_t which is uint32; -1 == 0xFFFFFFFF). # We treat any parent_idx >= iseq_count or parent_idx == idx as "no real parent". all_functions.each_with_index do |fn, idx| parent_idx = fn.misc[:parent_iseq_index] # Clamp to signed 32-bit to handle the -1 sentinel (stored as 0xFFFFFFFF). # small_value is decoded as unsigned; -1 as uint32 = 4294967295. parent_idx_signed = parent_idx > 0x7FFFFFFF ? parent_idx - 0x100000000 : parent_idx next if parent_idx_signed < 0 # -1 sentinel: no parent next if parent_idx_signed == idx # root iseq: parent is itself next if parent_idx_signed >= iseq_count parent_fn = all_functions[parent_idx_signed] parent_fn&.children&.push(fn) end # The root is the top-level iseq (index 0 in the list, or the one with no real parent). # Convention: iseq-list index 0 is always the outermost iseq. root = all_functions[0] new(all_functions, root, raw_offset_array, object_table, raw_trailing) end |
Instance Method Details
#encode(writer) ⇒ Object
Encode the iseq list into writer.
Writes each function’s data sections sequentially at writer.pos, tracking fresh absolute byte offsets in data_region_offsets. IR-owned sections (bytecode, catch_table, line_info, opt_table) are re-encoded from IR; raw-only sections (kw, local_table, lvar_states, ci_entries, outer_vars) are written verbatim from per-section raw captures stored at decode time.
After all data sections, each function’s body record is emitted at writer.pos using IseqEnvelope.encode with the freshly-computed offsets.
After all iseqs, the fresh body offsets are packed as the iseq offset array. The original offset array is compared against the fresh one; a mismatch raises a diagnostic error (indicating layout divergence for unmodified IR).
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 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 384 385 386 387 388 389 390 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 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 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 523 524 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 |
# File 'lib/optimize/codec/iseq_list.rb', line 217 def encode(writer) original_body_offsets = @raw_offset_array.unpack("V*") fresh_body_offsets = [] @functions.each_with_index do |fn, idx| misc = fn.misc # Build inst_to_slot once if this function has instructions. inst_to_slot = fn.instructions ? InstructionStream.inst_to_slot_map(fn.instructions) : nil # ci_entries harvested from InstructionStream.encode as it walks # CALLDATA operand slots. Populated in the bytecode section below, # consumed in the ci_entries section emit. ci_entries_out = [] # Fresh offsets for this function's data sections (all absolute byte offsets). dro = {} # Helper: emit a section only if it was present in the original (abs != nil and > 0). # # Emits: # 1. Alignment pad bytes (misc[:"#{key}_pad_raw"]) before the section content. # 2. The section content (from block). # 3. Trailing zero bytes to pad the emitted content up to the original raw size. # This preserves downstream body-record offsets (all sections stay at the same # absolute positions as in the original binary). # - For bytecode: raw == content exactly, so no trailing pad. # - For raw-only sections (kw, local_table, etc.): raw == content, no pad. # - For IR-encoded sections where size is unchanged: content bytes match raw; # any trailing alignment zeros are already embedded in raw and reproduced. # - For IR-encoded sections where count is filtered (insns_info, catch_table): # content may be SHORTER than raw. The trailing zeros are safe because the # body record's insns_info_size / catch_table_size fields are patched to the # filtered count, so Ruby's loader reads only the live entries and ignores # the trailing zero bytes. Pass allow_content_change: true to suppress the # byte-identity assertion for these sections. # # Records fresh abs offset in dro[abs_key]. # Raises on byte-identity mismatch (unless allow_content_change: true). emit_section = ->(key, abs_key, allow_content_change: false, &block) do orig_abs = misc[abs_key] return unless orig_abs && orig_abs > 0 # Emit alignment padding before this section. pad_raw = misc[:"#{key}_pad_raw"] || "".b writer.write_bytes(pad_raw) unless pad_raw.empty? # Record fresh abs offset (position after padding = start of content). dro[abs_key] = writer.pos # Emit section content (block writes to writer and returns content bytes). content_bytes = block.call # Zero-pad IR content to original raw size so that all subsequent sections # land at the same absolute offsets as in the original binary. This is always # safe: for raw-verbatim sections pad is 0; for IR-encoded sections with an # unchanged count it is also 0 (sizes match); for filtered sections (insns_info, # catch_table) the trailing zeros are ignored by the loader because the # body-record size fields are patched to the filtered count. original_raw = misc[:"#{key}_raw"] || "".b trailing_pad = original_raw.bytesize - content_bytes.bytesize if trailing_pad > 0 writer.write_bytes("\x00".b * trailing_pad) end # Byte-identity assertion for unmodified (raw-verbatim or same-count IR) sections. # Skipped for IR-encoded sections whose entry count may legitimately decrease # (insns_info body/positions, catch_table) — those pass allow_content_change: true. unless allow_content_change emitted_str = content_bytes + ("\x00".b * [trailing_pad, 0].max) check_len = original_raw.bytesize if emitted_str.bytesize != check_len || emitted_str != original_raw first_diff = (0...check_len).find { |i| emitted_str.getbyte(i) != original_raw.getbyte(i) } raise RuntimeError, "byte-identity assertion failed: iseq=#{fn.name} section=#{key} " \ "emitted=#{emitted_str.bytesize} original=#{check_len} " \ "first diff at offset #{first_diff} " \ "(emitted=0x#{emitted_str.getbyte(first_diff)&.to_s(16)&.rjust(2,'0') || 'nil'} " \ "original=0x#{original_raw.getbyte(first_diff)&.to_s(16)&.rjust(2,'0') || 'nil'})" end end end # 1. Bytecode (IR-encoded; raw is exact bytecode_size bytes, no trailing pad). # NOTE: bytecode is intentionally modifiable; length changes are supported. # We bypass emit_section for bytecode and write it directly, recording the # fresh bytecode_size in dro so the body record reflects the new size. bytecode_abs_orig = misc[:bytecode_abs] if bytecode_abs_orig && bytecode_abs_orig > 0 pad_raw = misc[:bytecode_pad_raw] || "".b writer.write_bytes(pad_raw) unless pad_raw.empty? dro[:bytecode_abs] = writer.pos bytecode_size = misc[:bytecode_size] if bytecode_size && bytecode_size > 0 && fn.instructions new_bytes = InstructionStream.encode(fn.instructions, @object_table, @functions, ci_entries_out: ci_entries_out) writer.write_bytes(new_bytes) dro[:bytecode_size] = new_bytes.bytesize end end # 2. opt_table (IR-encoded; VALUE[] entries; raw may include trailing pad to next section). # NOTE: If fn.arg_positions == nil (no positional optional args), we cannot re-encode # from IR. For keyword-only iseqs (param_opt_num == 0) arg_positions is nil but # opt_table_abs may still be set; emit the raw slice to preserve round-trip. emit_section.call(:opt_table, :opt_table_abs) do arg_positions = fn.arg_positions if arg_positions && inst_to_slot content_writer = BinaryWriter.new ArgPositions.encode_to_writer(content_writer, arg_positions, inst_to_slot) writer.write_bytes(content_writer.buffer) content_writer.buffer else # No IR data: write original bytes verbatim. original_raw_opt = misc[:opt_table_raw] || "".b writer.write_bytes(original_raw_opt) original_raw_opt end end # 3. kw (keyword param struct — raw only; raw includes trailing pad to next section) emit_section.call(:kw, :kw_abs) do raw = misc[:kw_raw] || "".b writer.write_bytes(raw) raw end # Pre-compute LineInfo.encode once per function (when line_entries are present). line_entries = fn.line_entries insns_info_size = misc[:insns_info_size] line_body_bytes = nil line_pos_bytes = nil encoded_insns_info_size = nil if line_entries && insns_info_size && insns_info_size > 0 && inst_to_slot body_writer = BinaryWriter.new pos_writer = BinaryWriter.new LineInfo.encode(body_writer, pos_writer, line_entries, inst_to_slot) line_body_bytes = body_writer.buffer line_pos_bytes = pos_writer.buffer # Count the live entries (those whose inst is still in inst_to_slot). encoded_insns_info_size = line_entries.count { |e| inst_to_slot.key?(e.inst) } end # 4. insns_info body (IR-encoded; raw may include trailing pad) # allow_content_change: true because filtered entries produce fewer bytes than # original; the body-record insns_info_size is patched to the live count so # the loader only reads the live entries; trailing zeros are ignored. emit_section.call(:insns_body, :insns_body_abs, allow_content_change: true) do if line_body_bytes writer.write_bytes(line_body_bytes) line_body_bytes else "".b end end # 5. insns_info positions (IR-encoded; re-encode to get pos bytes) # allow_content_change: true for the same reason as insns_body above. emit_section.call(:insns_pos, :insns_pos_abs, allow_content_change: true) do if line_pos_bytes writer.write_bytes(line_pos_bytes) line_pos_bytes else "".b end end # 6. local_table (raw; raw includes trailing pad to next section) emit_section.call(:local_table, :local_table_abs) do raw = misc[:local_table_raw] || "".b writer.write_bytes(raw) raw end # 7. lvar_states (raw) emit_section.call(:lvar_states, :lvar_states_abs) do raw = misc[:lvar_states_raw] || "".b writer.write_bytes(raw) raw end # 8. catch_table (IR-encoded; raw may include trailing pad) # allow_content_change: true because dropped entries produce fewer bytes; # the body-record catch_table_size is patched to the live count. encoded_catch_table_size = nil emit_section.call(:catch_table, :catch_table_abs, allow_content_change: true) do catch_entries = fn.catch_entries catch_table_size = misc[:catch_table_size] if catch_entries && catch_table_size && catch_table_size > 0 && inst_to_slot ct_writer = BinaryWriter.new CatchTable.encode(ct_writer, catch_entries, inst_to_slot) writer.write_bytes(ct_writer.buffer) # Count live entries (those whose insts are all still present). encoded_catch_table_size = catch_entries.count do |e| inst_to_slot.key?(e.start_inst) && inst_to_slot.key?(e.end_inst) && (e.cont_inst.nil? || inst_to_slot.key?(e.cont_inst)) end ct_writer.buffer else "".b end end # 9. ci_entries (IR-encoded from records harvested by InstructionStream.encode # above). For unmodified IR, harvested records equal original ci_entries # and bytes match byte-for-byte (Task 1 proves CiEntries.encode(decode(raw)) # == raw). When passes have removed send-family instructions, the harvested # list is SHORTER than the original; we update ci_size in the body record # to the new count and suppress the byte-identity assertion. # Fallback to raw bytes when there are no instructions to harvest from # (e.g. iseqs whose bytecode section was absent). emit_section.call(:ci_entries, :ci_entries_abs, allow_content_change: true) do bytes = if fn.instructions CiEntries.encode(ci_entries_out) else misc[:ci_entries_raw] || "".b end writer.write_bytes(bytes) bytes end # Propagate the harvested ci_size so the body record reflects the # post-pass count. Only override when we actually harvested (i.e. the # function has instructions); otherwise the original ci_size is kept. if fn.instructions dro[:ci_size] = ci_entries_out.size end # 10. outer_vars (raw) emit_section.call(:outer_vars, :outer_vars_abs) do raw = misc[:outer_vars_raw] || "".b writer.write_bytes(raw) raw end # Emit body record at current writer.pos. dro[:body_offset_abs] = writer.pos # Propagate nil offsets for absent sections (so rel offsets compute correctly as 0). dro[:bytecode_abs] ||= misc[:bytecode_abs] dro[:opt_table_abs] ||= misc[:opt_table_abs] dro[:kw_abs] ||= misc[:kw_abs] dro[:insns_body_abs] ||= misc[:insns_body_abs] dro[:insns_pos_abs] ||= misc[:insns_pos_abs] dro[:local_table_abs] ||= misc[:local_table_abs] dro[:lvar_states_abs] ||= misc[:lvar_states_abs] dro[:catch_table_abs] ||= misc[:catch_table_abs] dro[:ci_entries_abs] ||= misc[:ci_entries_abs] dro[:outer_vars_abs] ||= misc[:outer_vars_abs] # Pass filtered insns_info_size when it was computed from IR (including 0, # which is truthy in Ruby — use nil? guard so 0 is written correctly when # all entries are filtered out). dro[:insns_info_size] = encoded_insns_info_size unless encoded_insns_info_size.nil? # Pass filtered catch_table_size when entries were dropped due to dangling refs. # Same nil? guard: 0 must be propagated if all entries are dropped. dro[:catch_table_size] = encoded_catch_table_size unless encoded_catch_table_size.nil? fresh_body_offsets << writer.pos # Encode body record from IR + fresh offsets. body_writer = BinaryWriter.new IseqEnvelope.encode(body_writer, fn, dro) emitted_body = body_writer.buffer original_body = misc[:raw_body] # When bytecode size is unchanged (unmodified IR), byte-identity holds and we # assert it to catch regressions. When bytecode size changed (length-changing edit), # the body record legitimately differs (new bytecode_size, new relative offsets, new # iseq_size), so we skip the identity assertion and trust the fresh dro values. bytecode_size_changed = dro.key?(:bytecode_size) && dro[:bytecode_size] != misc[:bytecode_size] # iseq_size (slot count) can change even when bytecode_size (byte count) is unchanged, # e.g. when a length-changing IR edit replaces instructions whose small_value-encoded # bytes happen to be equal length. Treat that as a legitimate change too. iseq_size_changed = fn.instructions && InstructionStream.total_slots(fn.instructions) != misc[:iseq_size] # local_table growth (LocalTable.grow!) shifts all sections after # local_table forward, so the body record's relative-offset fields # (and local_table_size itself) legitimately change even when # bytecode/iseq size is unchanged. grow! stashes the pre-growth size # in misc[:local_table_size_pre_growth] so we can detect this here. local_table_size_changed = misc[:local_table_size_pre_growth] && misc[:local_table_size_pre_growth] != misc[:local_table_size] unless bytecode_size_changed || iseq_size_changed || local_table_size_changed if emitted_body.bytesize != original_body.bytesize raise RuntimeError, "body record size mismatch: iseq=#{fn.name} was=#{original_body.bytesize} got=#{emitted_body.bytesize}" end if emitted_body != original_body # Field-by-field diff to identify the first wrong field. field_names = [ :type_val, :iseq_size, :bytecode_offset_rel, :bytecode_size, :param_flags, :param_size, :param_lead_num, :param_opt_num, :param_rest_start, :param_post_start, :param_post_num, :param_block_start, :param_opt_table_offset_rel, :param_keyword_offset, :location_pathobj_index, :location_base_label_index, :location_label_index, :location_first_lineno, :location_node_id, :location_beg_lineno, :location_beg_column, :location_end_lineno, :location_end_column, :insns_info_body_offset_rel, :insns_info_positions_offset_rel, :insns_info_size, :local_table_offset_rel, :lvar_states_offset_rel, :catch_table_size, :catch_table_offset_rel, :parent_iseq_index, :local_iseq_index, :mandatory_only_iseq_index, :ci_entries_offset_rel, :outer_variables_offset_rel, :variable_flip_count, :local_table_size, :ivc_size, :icvarc_size, :ise_size, :ic_size, :ci_size, :stack_max, :builtin_attrs, :prism, ] orig_reader = BinaryReader.new(original_body) emit_reader = BinaryReader.new(emitted_body) field_names.each do |field| orig_val = orig_reader.read_small_value emit_val = emit_reader.read_small_value if orig_val != emit_val raise RuntimeError, "body record field mismatch: iseq=#{fn.name} field=#{field} " \ "expected=#{orig_val} got=#{emit_val}" end end raise RuntimeError, "body record bytes differ but all fields match: iseq=#{fn.name} (encoding bug)" end end writer.write_bytes(emitted_body) end # Append trailing bytes after the last body record (alignment padding, if any). writer.write_bytes(@raw_trailing) # Align to 4 bytes before writing the iseq offset array # (ibf_dump_align uses sizeof(ibf_offset_t) = 4). writer.align_to(4) # Record the fresh iseq_list_offset (start of the offset array in the output). fresh_iseq_list_offset = writer.pos # Write the fresh iseq offset array. writer.write_bytes(fresh_body_offsets.pack("V*")) # Return the fresh iseq_list_offset so Codec.encode can patch the header. fresh_iseq_list_offset end |