unibuf 0.1.0 → 0.1.2

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +178 -330
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/README.adoc +443 -254
  5. data/docs/CAPNPROTO.adoc +436 -0
  6. data/docs/FLATBUFFERS.adoc +430 -0
  7. data/docs/PROTOBUF.adoc +515 -0
  8. data/docs/TXTPROTO.adoc +369 -0
  9. data/lib/unibuf/commands/convert.rb +60 -2
  10. data/lib/unibuf/commands/schema.rb +68 -11
  11. data/lib/unibuf/errors.rb +23 -26
  12. data/lib/unibuf/models/capnproto/enum_definition.rb +72 -0
  13. data/lib/unibuf/models/capnproto/field_definition.rb +81 -0
  14. data/lib/unibuf/models/capnproto/interface_definition.rb +70 -0
  15. data/lib/unibuf/models/capnproto/method_definition.rb +81 -0
  16. data/lib/unibuf/models/capnproto/schema.rb +84 -0
  17. data/lib/unibuf/models/capnproto/struct_definition.rb +96 -0
  18. data/lib/unibuf/models/capnproto/union_definition.rb +62 -0
  19. data/lib/unibuf/models/flatbuffers/enum_definition.rb +69 -0
  20. data/lib/unibuf/models/flatbuffers/field_definition.rb +88 -0
  21. data/lib/unibuf/models/flatbuffers/schema.rb +102 -0
  22. data/lib/unibuf/models/flatbuffers/struct_definition.rb +70 -0
  23. data/lib/unibuf/models/flatbuffers/table_definition.rb +73 -0
  24. data/lib/unibuf/models/flatbuffers/union_definition.rb +60 -0
  25. data/lib/unibuf/models/message.rb +10 -0
  26. data/lib/unibuf/models/values/scalar_value.rb +2 -2
  27. data/lib/unibuf/parsers/binary/wire_format_parser.rb +199 -19
  28. data/lib/unibuf/parsers/capnproto/binary_parser.rb +267 -0
  29. data/lib/unibuf/parsers/capnproto/grammar.rb +272 -0
  30. data/lib/unibuf/parsers/capnproto/list_reader.rb +208 -0
  31. data/lib/unibuf/parsers/capnproto/pointer_decoder.rb +163 -0
  32. data/lib/unibuf/parsers/capnproto/processor.rb +348 -0
  33. data/lib/unibuf/parsers/capnproto/segment_reader.rb +131 -0
  34. data/lib/unibuf/parsers/capnproto/struct_reader.rb +199 -0
  35. data/lib/unibuf/parsers/flatbuffers/binary_parser.rb +325 -0
  36. data/lib/unibuf/parsers/flatbuffers/grammar.rb +235 -0
  37. data/lib/unibuf/parsers/flatbuffers/processor.rb +299 -0
  38. data/lib/unibuf/parsers/textproto/grammar.rb +1 -1
  39. data/lib/unibuf/parsers/textproto/processor.rb +10 -0
  40. data/lib/unibuf/serializers/binary_serializer.rb +218 -0
  41. data/lib/unibuf/serializers/capnproto/binary_serializer.rb +402 -0
  42. data/lib/unibuf/serializers/capnproto/list_writer.rb +199 -0
  43. data/lib/unibuf/serializers/capnproto/pointer_encoder.rb +118 -0
  44. data/lib/unibuf/serializers/capnproto/segment_builder.rb +124 -0
  45. data/lib/unibuf/serializers/capnproto/struct_writer.rb +139 -0
  46. data/lib/unibuf/serializers/flatbuffers/binary_serializer.rb +167 -0
  47. data/lib/unibuf/validators/type_validator.rb +1 -1
  48. data/lib/unibuf/version.rb +1 -1
  49. data/lib/unibuf.rb +27 -0
  50. metadata +36 -1
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../models/capnproto/schema"
4
+ require_relative "../../models/capnproto/struct_definition"
5
+ require_relative "../../models/capnproto/field_definition"
6
+ require_relative "../../models/capnproto/enum_definition"
7
+ require_relative "../../models/capnproto/interface_definition"
8
+ require_relative "../../models/capnproto/method_definition"
9
+ require_relative "../../models/capnproto/union_definition"
10
+
11
+ module Unibuf
12
+ module Parsers
13
+ module Capnproto
14
+ # Processor to transform Cap'n Proto AST to Schema models
15
+ class Processor
16
+ class << self
17
+ def process(ast)
18
+ return Models::Capnproto::Schema.new unless ast
19
+
20
+ elements = Array(ast)
21
+
22
+ attributes = {
23
+ file_id: extract_file_id(elements),
24
+ usings: extract_usings(elements),
25
+ structs: extract_structs(elements),
26
+ enums: extract_enums(elements),
27
+ interfaces: extract_interfaces(elements),
28
+ constants: extract_constants(elements),
29
+ }
30
+
31
+ Models::Capnproto::Schema.new(attributes)
32
+ end
33
+
34
+ private
35
+
36
+ def extract_file_id(elements)
37
+ file_id_element = elements.find { |el| el.key?(:file_id) }
38
+ return nil unless file_id_element
39
+
40
+ file_id_element[:file_id][:number].to_s
41
+ end
42
+
43
+ def extract_usings(elements)
44
+ elements.select { |el| el.key?(:using) }.map do |el|
45
+ {
46
+ alias: el[:using][:alias][:identifier].to_s,
47
+ import_path: el[:using][:import_path][:string].to_s,
48
+ }
49
+ end
50
+ end
51
+
52
+ def extract_structs(elements)
53
+ elements.select { |el| el.key?(:struct) }.map do |el|
54
+ process_struct(el[:struct])
55
+ end
56
+ end
57
+
58
+ def extract_enums(elements)
59
+ elements.select { |el| el.key?(:enum) }.map do |el|
60
+ process_enum(el[:enum])
61
+ end
62
+ end
63
+
64
+ def extract_interfaces(elements)
65
+ elements.select { |el| el.key?(:interface) }.map do |el|
66
+ process_interface(el[:interface])
67
+ end
68
+ end
69
+
70
+ def extract_constants(elements)
71
+ elements.select { |el| el.key?(:const) }.map do |el|
72
+ process_const(el[:const])
73
+ end
74
+ end
75
+
76
+ def process_struct(struct_data)
77
+ name = struct_data[:struct_name][:identifier].to_s
78
+ body = struct_data[:body]
79
+ annotations = extract_annotations(struct_data[:annotation])
80
+
81
+ fields = extract_struct_fields(body)
82
+ unions = extract_unions(body)
83
+ groups = extract_groups(body)
84
+ nested_structs = extract_nested_structs(body)
85
+ nested_enums = extract_nested_enums(body)
86
+ nested_interfaces = extract_nested_interfaces(body)
87
+
88
+ Models::Capnproto::StructDefinition.new(
89
+ name: name,
90
+ fields: fields,
91
+ unions: unions,
92
+ groups: groups,
93
+ nested_structs: nested_structs,
94
+ nested_enums: nested_enums,
95
+ nested_interfaces: nested_interfaces,
96
+ annotations: annotations,
97
+ )
98
+ end
99
+
100
+ def extract_struct_fields(body)
101
+ return [] unless body
102
+
103
+ Array(body).select do |el|
104
+ el.respond_to?(:key?) && el.key?(:field)
105
+ end.map do |el|
106
+ process_field(el[:field])
107
+ end
108
+ end
109
+
110
+ def extract_unions(body)
111
+ return [] unless body
112
+
113
+ Array(body).select do |el|
114
+ el.respond_to?(:key?) && el.key?(:union)
115
+ end.map do |el|
116
+ process_union(el[:union])
117
+ end
118
+ end
119
+
120
+ def extract_groups(body)
121
+ return [] unless body
122
+
123
+ Array(body).select do |el|
124
+ el.respond_to?(:key?) && el.key?(:group)
125
+ end.map do |el|
126
+ process_group(el[:group])
127
+ end
128
+ end
129
+
130
+ def extract_nested_structs(body)
131
+ return [] unless body
132
+
133
+ Array(body).select do |el|
134
+ el.respond_to?(:key?) && el.key?(:nested_struct)
135
+ end.map do |el|
136
+ process_struct(el[:nested_struct])
137
+ end
138
+ end
139
+
140
+ def extract_nested_enums(body)
141
+ return [] unless body
142
+
143
+ Array(body).select do |el|
144
+ el.respond_to?(:key?) && el.key?(:nested_enum)
145
+ end.map do |el|
146
+ process_enum(el[:nested_enum])
147
+ end
148
+ end
149
+
150
+ def extract_nested_interfaces(body)
151
+ return [] unless body
152
+
153
+ Array(body).select do |el|
154
+ el.respond_to?(:key?) && el.key?(:nested_interface)
155
+ end.map do |el|
156
+ process_interface(el[:nested_interface])
157
+ end
158
+ end
159
+
160
+ def process_field(field_data)
161
+ name = field_data[:name][:identifier].to_s
162
+ ordinal = field_data[:ordinal][:number].to_s.to_i
163
+ type = process_field_type(field_data[:type])
164
+ default_value = process_default_value(field_data[:default])
165
+
166
+ Models::Capnproto::FieldDefinition.new(
167
+ name: name,
168
+ ordinal: ordinal,
169
+ type: type,
170
+ default_value: default_value,
171
+ )
172
+ end
173
+
174
+ def process_field_type(type_data)
175
+ if type_data[:generic]
176
+ # Generic type: List(T)
177
+ {
178
+ generic: "List",
179
+ element_type: process_field_type(type_data[:generic][:element_type]),
180
+ }
181
+ elsif type_data[:primitive_type]
182
+ type_data[:primitive_type].to_s
183
+ else
184
+ type_data[:user_type][:identifier].to_s
185
+ end
186
+ end
187
+
188
+ def process_default_value(default_data)
189
+ return nil unless default_data
190
+
191
+ if default_data[:number]
192
+ val = default_data[:number].to_s
193
+ val.include?(".") ? val.to_f : val.to_i
194
+ elsif default_data[:bool]
195
+ default_data[:bool].to_s == "true"
196
+ elsif default_data[:string]
197
+ default_data[:string].to_s
198
+ end
199
+ end
200
+
201
+ def process_union(union_data)
202
+ fields = extract_struct_fields(union_data[:fields])
203
+
204
+ Models::Capnproto::UnionDefinition.new(
205
+ fields: fields,
206
+ )
207
+ end
208
+
209
+ def process_group(group_data)
210
+ name = group_data[:name][:identifier].to_s
211
+ ordinal = group_data[:ordinal][:number].to_s.to_i
212
+ fields = extract_struct_fields(group_data[:fields])
213
+
214
+ {
215
+ name: name,
216
+ ordinal: ordinal,
217
+ fields: fields.map(&:to_h),
218
+ }
219
+ end
220
+
221
+ def process_enum(enum_data)
222
+ name = enum_data[:enum_name][:identifier].to_s
223
+ annotations = extract_annotations(enum_data[:annotation])
224
+ values = {}
225
+
226
+ Array(enum_data[:values]).each do |val_el|
227
+ next unless val_el.respond_to?(:key?)
228
+
229
+ val_name = val_el[:name][:identifier].to_s
230
+ val_ordinal = val_el[:ordinal][:number].to_s.to_i
231
+
232
+ values[val_name] = val_ordinal
233
+ end
234
+
235
+ Models::Capnproto::EnumDefinition.new(
236
+ name: name,
237
+ values: values,
238
+ annotations: annotations,
239
+ )
240
+ end
241
+
242
+ def process_interface(interface_data)
243
+ name = interface_data[:interface_name][:identifier].to_s
244
+ annotations = extract_annotations(interface_data[:annotation])
245
+ body = interface_data[:body]
246
+
247
+ methods = extract_methods(body)
248
+
249
+ Models::Capnproto::InterfaceDefinition.new(
250
+ name: name,
251
+ methods: methods,
252
+ annotations: annotations,
253
+ )
254
+ end
255
+
256
+ def extract_methods(body)
257
+ return [] unless body
258
+
259
+ Array(body).select do |el|
260
+ el.respond_to?(:key?) && el.key?(:method)
261
+ end.map do |el|
262
+ process_method(el[:method])
263
+ end
264
+ end
265
+
266
+ def process_method(method_data)
267
+ name = method_data[:name][:identifier].to_s
268
+ ordinal = method_data[:ordinal][:number].to_s.to_i
269
+ params = extract_params(method_data[:params])
270
+ results = extract_params(method_data[:results])
271
+
272
+ Models::Capnproto::MethodDefinition.new(
273
+ name: name,
274
+ ordinal: ordinal,
275
+ params: params,
276
+ results: results,
277
+ )
278
+ end
279
+
280
+ def extract_params(params_data)
281
+ return [] unless params_data
282
+
283
+ Array(params_data).select do |el|
284
+ el.respond_to?(:key?) && el.key?(:param)
285
+ end.map do |el|
286
+ param = el[:param]
287
+ {
288
+ name: param[:name][:identifier].to_s,
289
+ type: process_field_type(param[:type]),
290
+ }
291
+ end
292
+ end
293
+
294
+ def process_const(const_data)
295
+ {
296
+ name: const_data[:name][:identifier].to_s,
297
+ type: process_field_type(const_data[:type]),
298
+ value: process_const_value(const_data[:value]),
299
+ }
300
+ end
301
+
302
+ def process_const_value(value_data)
303
+ if value_data[:number]
304
+ val = value_data[:number].to_s
305
+ val.include?(".") ? val.to_f : val.to_i
306
+ elsif value_data[:bool]
307
+ value_data[:bool].to_s == "true"
308
+ elsif value_data[:string]
309
+ value_data[:string].to_s
310
+ elsif value_data[:ref]
311
+ value_data[:ref][:identifier].to_s
312
+ end
313
+ end
314
+
315
+ def extract_annotations(annotation_data)
316
+ return [] unless annotation_data
317
+
318
+ Array(annotation_data).map do |ann|
319
+ name = ann[:annotation][:identifier].to_s
320
+ value = if ann[:value]
321
+ process_annotation_value(ann[:value])
322
+ else
323
+ true
324
+ end
325
+
326
+ { name: name, value: value }
327
+ end
328
+ end
329
+
330
+ def process_annotation_value(value_data)
331
+ if value_data[:number]
332
+ val = value_data[:number].to_s
333
+ val.include?(".") ? val.to_f : val.to_i
334
+ elsif value_data[:bool]
335
+ value_data[:bool].to_s == "true"
336
+ elsif value_data[:string]
337
+ value_data[:string].to_s
338
+ elsif value_data[:identifier]
339
+ value_data[:identifier].to_s
340
+ else
341
+ value_data.to_s
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unibuf
4
+ module Parsers
5
+ module Capnproto
6
+ # Reader for Cap'n Proto binary segments
7
+ # Cap'n Proto uses word-aligned (8-byte) segments for memory management
8
+ class SegmentReader
9
+ WORD_SIZE = 8 # Cap'n Proto uses 8-byte words
10
+
11
+ attr_reader :segments, :segment_count
12
+
13
+ # Initialize with binary data
14
+ # @param data [String] Binary data to read
15
+ def initialize(data)
16
+ @data = data
17
+ @segments = []
18
+ @segment_count = 0
19
+ parse_segments
20
+ end
21
+
22
+ # Read a word (8 bytes) from a segment at given offset
23
+ # @param segment_id [Integer] Segment index
24
+ # @param word_offset [Integer] Word offset within segment
25
+ # @return [Integer] 64-bit word value
26
+ def read_word(segment_id, word_offset)
27
+ if segment_id >= @segment_count
28
+ raise ArgumentError,
29
+ "Invalid segment ID"
30
+ end
31
+ raise ArgumentError, "Invalid word offset" if word_offset.negative?
32
+
33
+ segment = @segments[segment_id]
34
+ byte_offset = word_offset * WORD_SIZE
35
+
36
+ if byte_offset + WORD_SIZE > segment.size
37
+ raise ArgumentError,
38
+ "Offset out of bounds"
39
+ end
40
+
41
+ # Read 8 bytes as little-endian 64-bit unsigned integer
42
+ segment[byte_offset, WORD_SIZE].unpack1("Q<")
43
+ end
44
+
45
+ # Read multiple words from a segment
46
+ # @param segment_id [Integer] Segment index
47
+ # @param word_offset [Integer] Starting word offset
48
+ # @param count [Integer] Number of words to read
49
+ # @return [Array<Integer>] Array of word values
50
+ def read_words(segment_id, word_offset, count)
51
+ (0...count).map { |i| read_word(segment_id, word_offset + i) }
52
+ end
53
+
54
+ # Read bytes from a segment
55
+ # @param segment_id [Integer] Segment index
56
+ # @param byte_offset [Integer] Byte offset within segment
57
+ # @param length [Integer] Number of bytes to read
58
+ # @return [String] Binary data
59
+ def read_bytes(segment_id, byte_offset, length)
60
+ if segment_id >= @segment_count
61
+ raise ArgumentError,
62
+ "Invalid segment ID"
63
+ end
64
+
65
+ segment = @segments[segment_id]
66
+ if byte_offset + length > segment.size
67
+ raise ArgumentError,
68
+ "Offset out of bounds"
69
+ end
70
+
71
+ segment[byte_offset, length]
72
+ end
73
+
74
+ # Get segment size in words
75
+ # @param segment_id [Integer] Segment index
76
+ # @return [Integer] Size in words
77
+ def segment_size(segment_id)
78
+ if segment_id >= @segment_count
79
+ raise ArgumentError,
80
+ "Invalid segment ID"
81
+ end
82
+
83
+ @segments[segment_id].size / WORD_SIZE
84
+ end
85
+
86
+ # Check if a segment exists
87
+ # @param segment_id [Integer] Segment index
88
+ # @return [Boolean]
89
+ def segment_exists?(segment_id)
90
+ segment_id >= 0 && segment_id < @segment_count
91
+ end
92
+
93
+ private
94
+
95
+ def parse_segments
96
+ return if @data.nil? || @data.empty?
97
+
98
+ # Read segment count (first 4 bytes as little-endian 32-bit integer)
99
+ # Segment count: (N + 1) where N is in the first 4 bytes
100
+ segment_count_minus_one = @data[0, 4].unpack1("L<")
101
+ @segment_count = segment_count_minus_one + 1
102
+
103
+ # Read segment sizes (each is a 4-byte little-endian integer)
104
+ # Segment sizes start at byte 4
105
+ offset = 4
106
+ segment_sizes = []
107
+
108
+ @segment_count.times do
109
+ size = @data[offset, 4].unpack1("L<")
110
+ segment_sizes << size
111
+ offset += 4
112
+ end
113
+
114
+ # Align to 8-byte boundary after segment table
115
+ # Header size is 4 + (segment_count * 4) = 4 * (1 + segment_count)
116
+ # If segment count is ODD, header is divisible by 8 (no padding needed)
117
+ # If segment count is EVEN, header needs 4 bytes padding to align to 8
118
+ offset += 4 unless @segment_count.odd?
119
+
120
+ # Read each segment
121
+ segment_sizes.each do |size_in_words|
122
+ size_in_bytes = size_in_words * WORD_SIZE
123
+ segment_data = @data[offset, size_in_bytes]
124
+ @segments << segment_data
125
+ offset += size_in_bytes
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "segment_reader"
4
+ require_relative "pointer_decoder"
5
+
6
+ module Unibuf
7
+ module Parsers
8
+ module Capnproto
9
+ # Reader for Cap'n Proto struct data
10
+ # Structs have two sections: data (inline primitives) and pointers
11
+ class StructReader
12
+ attr_reader :segment_reader, :segment_id, :word_offset, :data_words,
13
+ :pointer_words
14
+
15
+ # Initialize struct reader
16
+ # @param segment_reader [SegmentReader] Segment reader
17
+ # @param segment_id [Integer] Segment containing the struct
18
+ # @param word_offset [Integer] Word offset of struct start
19
+ # @param data_words [Integer] Number of data words
20
+ # @param pointer_words [Integer] Number of pointer words
21
+ def initialize(segment_reader, segment_id, word_offset, data_words,
22
+ pointer_words)
23
+ @segment_reader = segment_reader
24
+ @segment_id = segment_id
25
+ @word_offset = word_offset
26
+ @data_words = data_words
27
+ @pointer_words = pointer_words
28
+ end
29
+
30
+ # Read a primitive field from data section
31
+ # @param word_index [Integer] Word index in data section
32
+ # @param bit_offset [Integer] Bit offset within word (0-63)
33
+ # @param bit_width [Integer] Width in bits
34
+ # @return [Integer] Field value
35
+ def read_data_field(word_index, bit_offset = 0, bit_width = 64)
36
+ return 0 if word_index >= @data_words
37
+
38
+ word = @segment_reader.read_word(@segment_id,
39
+ @word_offset + word_index)
40
+
41
+ # Extract bits
42
+ mask = (1 << bit_width) - 1
43
+ (word >> bit_offset) & mask
44
+ end
45
+
46
+ # Read an 8-bit integer
47
+ # @param word_index [Integer] Word index
48
+ # @param byte_offset [Integer] Byte offset within word (0-7)
49
+ # @return [Integer]
50
+ def read_int8(word_index, byte_offset = 0)
51
+ value = read_data_field(word_index, byte_offset * 8, 8)
52
+ # Convert to signed
53
+ value >= 128 ? value - 256 : value
54
+ end
55
+
56
+ # Read an unsigned 8-bit integer
57
+ # @param word_index [Integer] Word index
58
+ # @param byte_offset [Integer] Byte offset within word (0-7)
59
+ # @return [Integer]
60
+ def read_uint8(word_index, byte_offset = 0)
61
+ read_data_field(word_index, byte_offset * 8, 8)
62
+ end
63
+
64
+ # Read a 16-bit integer
65
+ # @param word_index [Integer] Word index
66
+ # @param half_word_offset [Integer] Half-word offset (0-3)
67
+ # @return [Integer]
68
+ def read_int16(word_index, half_word_offset = 0)
69
+ value = read_data_field(word_index, half_word_offset * 16, 16)
70
+ # Convert to signed
71
+ value >= 32768 ? value - 65536 : value
72
+ end
73
+
74
+ # Read an unsigned 16-bit integer
75
+ # @param word_index [Integer] Word index
76
+ # @param half_word_offset [Integer] Half-word offset (0-3)
77
+ # @return [Integer]
78
+ def read_uint16(word_index, half_word_offset = 0)
79
+ read_data_field(word_index, half_word_offset * 16, 16)
80
+ end
81
+
82
+ # Read a 32-bit integer
83
+ # @param word_index [Integer] Word index
84
+ # @param dword_offset [Integer] Double-word offset (0-1)
85
+ # @return [Integer]
86
+ def read_int32(word_index, dword_offset = 0)
87
+ value = read_data_field(word_index, dword_offset * 32, 32)
88
+ # Convert to signed
89
+ value >= 2147483648 ? value - 4294967296 : value
90
+ end
91
+
92
+ # Read an unsigned 32-bit integer
93
+ # @param word_index [Integer] Word index
94
+ # @param dword_offset [Integer] Double-word offset (0-1)
95
+ # @return [Integer]
96
+ def read_uint32(word_index, dword_offset = 0)
97
+ read_data_field(word_index, dword_offset * 32, 32)
98
+ end
99
+
100
+ # Read a 64-bit integer
101
+ # @param word_index [Integer] Word index
102
+ # @return [Integer]
103
+ def read_int64(word_index)
104
+ value = read_data_field(word_index, 0, 64)
105
+ # Convert to signed
106
+ value >= 9223372036854775808 ? value - 18446744073709551616 : value
107
+ end
108
+
109
+ # Read an unsigned 64-bit integer
110
+ # @param word_index [Integer] Word index
111
+ # @return [Integer]
112
+ def read_uint64(word_index)
113
+ read_data_field(word_index, 0, 64)
114
+ end
115
+
116
+ # Read a 32-bit float
117
+ # @param word_index [Integer] Word index
118
+ # @param dword_offset [Integer] Double-word offset (0-1)
119
+ # @return [Float]
120
+ def read_float32(word_index, dword_offset = 0)
121
+ bits = read_uint32(word_index, dword_offset)
122
+ [bits].pack("L").unpack1("f")
123
+ end
124
+
125
+ # Read a 64-bit float
126
+ # @param word_index [Integer] Word index
127
+ # @return [Float]
128
+ def read_float64(word_index)
129
+ bits = read_uint64(word_index)
130
+ [bits].pack("Q").unpack1("d")
131
+ end
132
+
133
+ # Read a boolean
134
+ # @param word_index [Integer] Word index
135
+ # @param bit_offset [Integer] Bit offset within word
136
+ # @return [Boolean]
137
+ def read_bool(word_index, bit_offset = 0)
138
+ read_data_field(word_index, bit_offset, 1) == 1
139
+ end
140
+
141
+ # Read a pointer from pointer section
142
+ # @param pointer_index [Integer] Pointer index in pointer section
143
+ # @return [Hash, nil] Decoded pointer or nil
144
+ def read_pointer(pointer_index)
145
+ return nil if pointer_index >= @pointer_words
146
+
147
+ pointer_word_offset = @word_offset + @data_words + pointer_index
148
+ pointer_word = @segment_reader.read_word(@segment_id,
149
+ pointer_word_offset)
150
+
151
+ return nil if pointer_word.zero?
152
+
153
+ PointerDecoder.decode(pointer_word)
154
+ end
155
+
156
+ # Follow a pointer to get the target location
157
+ # @param pointer_index [Integer] Pointer index
158
+ # @return [Hash, nil] Target location info or nil
159
+ def follow_pointer(pointer_index)
160
+ pointer = read_pointer(pointer_index)
161
+ return nil unless pointer
162
+ return nil if pointer[:type] == :null
163
+
164
+ case pointer[:type]
165
+ when :struct
166
+ # Struct pointer points relative to its own position
167
+ pointer_position = @word_offset + @data_words + pointer_index
168
+ target_offset = pointer_position + 1 + pointer[:offset]
169
+ {
170
+ type: :struct,
171
+ segment_id: @segment_id,
172
+ word_offset: target_offset,
173
+ data_words: pointer[:data_words],
174
+ pointer_words: pointer[:pointer_words],
175
+ }
176
+ when :list
177
+ # List pointer points relative to its own position
178
+ pointer_position = @word_offset + @data_words + pointer_index
179
+ target_offset = pointer_position + 1 + pointer[:offset]
180
+ {
181
+ type: :list,
182
+ segment_id: @segment_id,
183
+ word_offset: target_offset,
184
+ element_size: pointer[:element_size],
185
+ element_count: pointer[:element_count],
186
+ }
187
+ when :far
188
+ # Far pointer points to another segment
189
+ {
190
+ type: :far,
191
+ segment_id: pointer[:segment_id],
192
+ word_offset: pointer[:offset],
193
+ }
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end