Class: Capnp::Generator

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/capnp/generator/generator.rb

Instance Method Summary collapse

Constructor Details

#initialize(request_ref) ⇒ Generator

Returns a new instance of Generator.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/capnp/generator/generator.rb', line 11

def initialize(request_ref)
  request = Schema::CodeGeneratorRequest.from_pointer(request_ref)
  raise "Invalid CodeGeneratorRequest" if request.nil?

  nodes = request.nodes
  raise "No nodes found" if nodes.nil?

  requested_files = request.requested_files
  raise "No requested files found" if requested_files.nil?

  requested_file_ids = requested_files.map(&:id)

  # Build a hash of nodes by id and gather all requested file nodes
  @nodes_by_id = T.let({}, T::Hash[Integer, Schema::Node])
  @files = T.let([], T::Array[Schema::Node])
  nodes.each do |node|
    @nodes_by_id[node.id] = node
    if node.is_file? && requested_file_ids.include?(node.id)
      @files << node
    end
  end

  # Gather all nodes that will become classes
  @node_to_class_path = T.let({}, T::Hash[Integer, T::Array[String]])
  @files.each do |file|
    name = file_to_module_name(file)
    @node_to_class_path.merge!(find_classes(file, [name]))
  end
end

Instance Method Details

#class_name(name) ⇒ Object



65
# File 'lib/capnp/generator/generator.rb', line 65

def class_name(name) = "#{name[0]&.upcase}#{name[1..]}"

#const_name(name) ⇒ Object



73
# File 'lib/capnp/generator/generator.rb', line 73

def const_name(name) = name.gsub(/([^A-Z])([A-Z]+)/, '\1_\2').upcase

#create_struct_to_obj(fields) ⇒ Object



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
# File 'lib/capnp/generator/generator.rb', line 232

def create_struct_to_obj(fields)
  # Split up union and non-union fields
  normal, union = fields
    .sort_by(&:code_order)
    .partition { _1.discriminant_value == Schema::Field::NO_DISCRIMINANT }

  # Process normal fields
  assignments = create_struct_to_obj_assignments(normal).map { "  #{_2}" }

  # Process union fields with a case statement
  union_assignments = if union.empty?
    []
  else
    whens = create_struct_to_obj_assignments(union).map do |name, assignment|
      "  when Which::#{class_name(name)} then #{assignment}"
    end
    [
      "  case which?",
      *whens,
      "  end"
    ]
  end

  [
    "sig { override.returns(Object) }",
    "def to_obj",
    "  res = {}",
    *assignments,
    *union_assignments,
    "  res",
    "end"
  ]
end

#create_struct_to_obj_assignments(fields) ⇒ Object



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
# File 'lib/capnp/generator/generator.rb', line 201

def create_struct_to_obj_assignments(fields)
  fields.map do |field|
    name = field.name&.to_s
    raise "Field without a name" if name.nil?

    mname = method_name(name)

    assignment = if field.is_group?
      # Group "fields" are treated as nested structs
      "res[#{mname.inspect}] = #{mname}.to_obj"
    else
      # Normal (non-group) fields
      type = field.slot.type
      raise "Field without a type" if type.nil?

      case type.which?
      when Schema::Type::Which::Text, Schema::Type::Which::Data, Schema::Type::Which::List, Schema::Type::Which::Struct
        "res[#{mname.inspect}] = #{mname}&.to_obj"
      when Schema::Type::Which::Interface, Schema::Type::Which::AnyPointer
        warn "Interfaces and AnyPointers cannot be converted to objects"
        "res[#{mname.inspect}] = #{mname}"
      else
        "res[#{mname.inspect}] = #{mname}"
      end
    end

    [name, assignment]
  end
end

#file_to_module_name(file) ⇒ Object



76
# File 'lib/capnp/generator/generator.rb', line 76

def file_to_module_name(file) = class_name(file.display_name&.to_s&.split("/")&.last&.sub(".capnp", "") || "")

#find_classes(node, path) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/capnp/generator/generator.rb', line 42

def find_classes(node, path)
  # Skip constants and annotations
  return {} if node.is_const? || node.is_annotation?

  result = T.let({node.id => path}, T::Hash[Integer, T::Array[String]])

  nested_nodes = node.nested_nodes
  return result if nested_nodes.nil?

  # Recurse into nested nodes
  nested_nodes.each do |nestednode|
    name = nestednode.name&.to_s
    raise "Node without a name" if name.nil?

    new_path = path + [name]
    result.merge!(find_classes(@nodes_by_id.fetch(nestednode.id), new_path))
  end

  result
end

#generateObject



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
# File 'lib/capnp/generator/generator.rb', line 79

def generate
  @files.each do |file|
    nested_nodes = file.nested_nodes
    next "" if nested_nodes.nil?

    nested_nodes_code = nested_nodes.flat_map do |nestednode|
      name = nestednode.name&.to_s
      raise "Node without a name" if name.nil?
      node = @nodes_by_id[nestednode.id]
      raise "Node not found" if node.nil?
      process_node(name, node)
    end

    code = [
      "# typed: strict",
      "",
      'require "capnp"',
      'require "sorbet-runtime"',
      "module #{file_to_module_name(file)}",
      *nested_nodes_code.map { "  #{_1}" }[1..],
      "end",
      ""
    ].map(&:rstrip).join("\n")

    # TODO: Use RedquestedFile.filename
    path = "#{file.display_name&.to_s}.rb"
    File.write(path, code)
  end
end

#method_name(name) ⇒ Object



69
# File 'lib/capnp/generator/generator.rb', line 69

def method_name(name) = name.gsub(/([^A-Z])([A-Z]+)/, '\1_\2').downcase

#process_enum(name, node) ⇒ Object



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/capnp/generator/generator.rb', line 491

def process_enum(name, node)
  raise "Nested nodes not supported in enum" unless node.nested_nodes&.length.to_i.zero?
  raise "Generic structs are not supported" if node.is_generic

  enumerants = node.enum.enumerants
  raise "No enumerants found" if enumerants.nil?

  # Enumerants are ordered by their numeric value
  enums = enumerants.map do |enumerant|
    warn "Ignoring annotations" unless enumerant.annotations&.length.to_i.zero?

    enumerant_name = enumerant.name&.to_s
    raise "Enumerant without a name" if enumerant_name.nil?
    enumerant_name
  end

  process_enumeration(name, enums)
end

#process_enumeration(name, enumerants) ⇒ Object



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
# File 'lib/capnp/generator/generator.rb', line 511

def process_enumeration(name, enumerants)
  definitions = T.let([], T::Array[String])
  from_int = T.let([], T::Array[String])

  # Enumerants are ordered by their numeric value
  enumerants.each_with_index do |enumerant_name, ix|
    ename = class_name(enumerant_name)
    definitions << "    #{ename} = new(#{enumerant_name.inspect})"
    from_int << "    when #{ix} then #{ename}"
  end

  # TODO: Define an Capnp::Enum class
  class_name = class_name(name)
  [
    "",
    "class #{class_name} < T::Enum",
    "  extend T::Sig",
    "  enums do",
    *definitions,
    "  end",
    "  sig { params(value: Integer).returns(#{class_name}) }",
    "  def self.from_integer(value)",
    "    case value",
    *from_int,
    "    else raise \"Unknown #{name} value: \#{value}\"",
    "    end",
    "  end",
    "end"
  ]
end

#process_field(field) ⇒ Object



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
# File 'lib/capnp/generator/generator.rb', line 267

def process_field(field)
  # TODO: Check union discriminant values
  warn "Ignoring annotations" unless field.annotations&.length.to_i.zero?

  name = field.name&.to_s
  raise "Field without a name" if name.nil?

  mname = method_name(name)

  getter_def = if field.is_group?
    group_node = @nodes_by_id.fetch(field.group.type_id)
    class_name = "Group#{class_name(name)}"
    group_class_code = process_struct(class_name, group_node)
    [
      "sig { returns(#{class_name}) }",
      "def #{mname} = #{class_name}.new(@data, @data_size, @pointers, @pointers_size)",
      *group_class_code
    ]
  else
    type = field.slot.type
    raise "Field without a type" if type.nil?

    default_variable = "DEFAULT_#{const_name(name)}"

    which_type = type.which?
    case which_type
    when Schema::Type::Which::Void
      [
        "sig { returns(NilClass) }",
        "def #{mname} = nil"
      ]
    when Schema::Type::Which::Bool
      default_value = field.slot.default_value&.bool ? "0xFF" : "0x00"
      offset = field.slot.offset / 8
      mask = (1 << (field.slot.offset % 8)).to_s(16)
      [
        "#{default_variable} = #{field.slot.default_value&.bool == true}",
        "sig { returns(T::Boolean) }",
        "def #{mname} = (read_u8(#{offset}, #{default_value}) & 0x#{mask}) != 0"
      ]
    when Schema::Type::Which::Int8
      default_value = field.slot.default_value&.int8 || 0
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_s8(#{field.slot.offset}, #{default_value})"
      ]
    when Schema::Type::Which::Int16
      default_value = field.slot.default_value&.int16 || 0
      offset = field.slot.offset * 2
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_s16(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Int32
      default_value = field.slot.default_value&.int32 || 0
      offset = field.slot.offset * 4
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_s32(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Int64
      default_value = field.slot.default_value&.int64 || 0
      offset = field.slot.offset * 8
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_s64(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Uint8
      default_value = field.slot.default_value&.uint8 || 0
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_u8(#{field.slot.offset}, #{default_value})"
      ]
    when Schema::Type::Which::Uint16
      default_value = field.slot.default_value&.uint16 || 0
      offset = field.slot.offset * 2
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_u16(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Uint32
      default_value = field.slot.default_value&.uint32 || 0
      offset = field.slot.offset * 4
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_u32(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Uint64
      default_value = field.slot.default_value&.uint64 || 0
      offset = field.slot.offset * 8
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Integer) }",
        "def #{mname} = read_u64(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Float32
      default_value = field.slot.default_value&.float32 || 0.0
      offset = field.slot.offset * 4
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Float) }",
        "def #{mname} = read_f32(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Float64
      default_value = field.slot.default_value&.float64 || 0.0
      offset = field.slot.offset * 8
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(Float) }",
        "def #{mname} = read_f64(#{offset}, #{default_value})"
      ]
    when Schema::Type::Which::Text
      default_value = field.slot.default_value&.text&.to_s.inspect
      apply_default = field.slot.had_explicit_default ? " || Capnp::ObjectString.new(#{default_variable})" : ""
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(T.nilable(Capnp::String)) }",
        "def #{mname} = Capnp::BufferString.from_pointer(read_pointer(#{field.slot.offset}))#{apply_default}"
      ]
    when Schema::Type::Which::Data
      default_value = field.slot.default_value&.data&.value.inspect
      apply_default = field.slot.had_explicit_default ? " || #{default_variable}" : ""
      [
        "#{default_variable} = #{default_value}",
        "sig { returns(T.nilable(Capnp::Data)) }",
        "def #{mname} = Capnp::Data.from_pointer(read_pointer(#{field.slot.offset}))#{apply_default}"
      ]
    when Schema::Type::Which::List
      raise "List default values not supported" if field.slot.had_explicit_default
      element_class = type.list.element_type
      raise "List without an element type" if element_class.nil?
      which_element_type = element_class.which?
      case which_element_type
      when Schema::Type::Which::Void
        raise "Void list elements not supported"
      when Schema::Type::Which::Bool
        raise "Bool list elements not supported"
      when Schema::Type::Which::Int8, Schema::Type::Which::Int16, Schema::Type::Which::Int32, Schema::Type::Which::Int64
        list_class = "Capnp::SignedIntegerList"
        element_class = "Integer"
      when Schema::Type::Which::Uint8, Schema::Type::Which::Uint16, Schema::Type::Which::Uint32, Schema::Type::Which::Uint64
        list_class = "Capnp::UnsignedIntegerList"
        element_class = "Integer"
      when Schema::Type::Which::Float32, Schema::Type::Which::Float64
        list_class = "Capnp::FloatList"
        element_class = "Float"
      when Schema::Type::Which::Text
        raise "Text list elements not supported"
      when Schema::Type::Which::Data
        raise "Data list elements not supported"
      when Schema::Type::Which::List
        raise "List list elements not supported"
      when Schema::Type::Which::Enum
        raise "Enum list elements not supported"
      when Schema::Type::Which::Struct
        raise "List[Struct] default values not supported" if field.slot.had_explicit_default
        element_class = @node_to_class_path.fetch(element_class.struct.type_id).join("::")
        list_class = "#{element_class}::List"
      when Schema::Type::Which::Interface
        raise "Interface list elements not supported"
      when Schema::Type::Which::AnyPointer
        raise "AnyPointer list elements not supported"
      else
        T.absurd(which_element_type)
      end

      [
        "sig { returns(T.nilable(Capnp::List[#{element_class}])) }",
        "def #{mname} = #{list_class}.from_pointer(read_pointer(#{field.slot.offset}))"
      ]
    when Schema::Type::Which::Enum
      enumerants = @nodes_by_id.fetch(type.enum.type_id).enum.enumerants
      raise "No enumerants found" if enumerants.nil?

      default_num = field.slot.default_value&.enum || 0
      default_value = class_name(enumerants[default_num]&.name&.to_s || "")

      offset = field.slot.offset * 2
      class_path = @node_to_class_path.fetch(type.enum.type_id).join("::")
      [
        # TODO: This doesn't work if the enum class is declared after this field
        "# #{default_variable} = #{class_path}::#{default_value}",
        "sig { returns(#{class_path}) }",
        "def #{mname} = #{class_path}.from_integer(read_u16(#{offset}, #{default_num}))"
      ]
    when Schema::Type::Which::Struct
      raise "Struct default values not supported" if field.slot.had_explicit_default
      class_path = @node_to_class_path.fetch(type.struct.type_id).join("::")
      [
        "sig { returns(T.nilable(#{class_path})) }",
        "def #{mname} = #{class_path}.from_pointer(read_pointer(#{field.slot.offset}))"
      ]
    when Schema::Type::Which::Interface
      raise "Interface fields not supported"
    when Schema::Type::Which::AnyPointer
      raise "Only unconstrained AnyPointers are supported" unless type.any_pointer.is_unconstrained?
      [
        "sig { returns(Capnp::Reference) }",
        "def #{mname} = read_pointer(#{field.slot.offset})"
      ]
    else
      T.absurd(which_type)
    end
  end

  # Add type checking methods for union fields
  if field.discriminant_value != Schema::Field::NO_DISCRIMINANT
    getter_def += [
      "sig { returns(T::Boolean) }",
      "def is_#{mname}? = which? == Which::#{class_name(name)}"
    ]
  end

  getter_def
end

#process_node(name, node) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/capnp/generator/generator.rb', line 110

def process_node(name, node)
  which_val = node.which?
  case which_val
  when Schema::Node::Which::Struct
    process_struct(name, node)
  when Schema::Node::Which::Enum
    process_enum(name, node)
  when Schema::Node::Which::Interface
    warn "Ignoring interface node"
    []
  when Schema::Node::Which::Const
    value = node.const.value
    raise "Const without a value" if value.nil?
    ["#{const_name(name)} = #{process_value(value)}"]
  when Schema::Node::Which::Annotation
    warn "Ignoring annotation node"
    []
  when Schema::Node::Which::File
    raise "Unexpected file node"
  else
    T.absurd(which_val)
  end
end

#process_struct(name, node) ⇒ Object



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
# File 'lib/capnp/generator/generator.rb', line 135

def process_struct(name, node)
  raise "Generic structs are not supported" if node.is_generic

  fields = node.struct.fields
  raise "No fields found" if fields.nil?

  field_code = fields.sort_by(&:code_order).flat_map do |field|
    process_field(field)
  end

  nested_node_code = node.nested_nodes&.flat_map do |nestednode|
    nested_node_name = nestednode.name&.to_s
    raise "Node without a name" if nested_node_name.nil?
    nested_node = @nodes_by_id.fetch(nestednode.id)

    process_node(nested_node_name, nested_node)
  end
  nested_node_code ||= []

  name = class_name(name)

  list_class_code = if node.struct.is_group
    []
  else
    [
      "",
      "  class List < Capnp::StructList",
      "    Elem = type_member { {fixed: #{name}} }",
      "    sig { override.returns(T.class_of(#{name})) }",
      "    def element_class = #{name}",
      "  end"
    ]
  end

  # Create Which enum class for unions
  which_code = if node.struct.discriminant_count.zero?
    []
  else
    discriminant_offset = node.struct.discriminant_offset * 2
    enumerants = fields
      .reject { _1.discriminant_value == Schema::Field::NO_DISCRIMINANT }
      .sort_by(&:discriminant_value)
      .map { _1.name&.to_s || "" }
    [
      "sig { returns(Which) }",
      "def which? = Which.from_integer(read_u16(#{discriminant_offset}, 0))",
      *process_enumeration("Which", enumerants)
    ]
  end

  # Create to_obj method
  to_obj_code = create_struct_to_obj(fields)

  [
    "",
    "class #{name} < Capnp::Struct",
    *field_code.map { "  #{_1}" },
    *nested_node_code.map { "  #{_1}" },
    *list_class_code,
    *which_code.map { "  #{_1}" },
    *to_obj_code.map { "  #{_1}" },
    "end"
  ]
end

#process_value(value) ⇒ Object



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
# File 'lib/capnp/generator/generator.rb', line 543

def process_value(value)
  which_value = value.which?
  case which_value
  when Schema::Value::Which::Void then nil
  when Schema::Value::Which::Bool then value.bool
  when Schema::Value::Which::Int8 then value.int8
  when Schema::Value::Which::Int16 then value.int16
  when Schema::Value::Which::Int32 then value.int32
  when Schema::Value::Which::Int64 then value.int64
  when Schema::Value::Which::Uint8 then value.uint8
  when Schema::Value::Which::Uint16 then value.uint16
  when Schema::Value::Which::Uint32 then value.uint32
  when Schema::Value::Which::Uint64 then value.uint64
  when Schema::Value::Which::Float32 then value.float32
  when Schema::Value::Which::Float64 then value.float64
  when Schema::Value::Which::Text then value.text&.to_s.inspect
  when Schema::Value::Which::Data then value.data&.value.inspect
  when Schema::Value::Which::List then raise "List values not supported"
  when Schema::Value::Which::Enum then value.enum # TODO: Convert to enum class
  when Schema::Value::Which::Struct then raise "Struct values not supported"
  when Schema::Value::Which::Interface then nil
  when Schema::Value::Which::AnyPointer then raise "AnyPointer values not supported"
  else T.absurd(which_value)
  end
end