Module: MilkTea::Bindgen::Generator::GeneratorEmitter

Included in:
MilkTea::Bindgen::Generator
Defined in:
lib/milk_tea/bindings/bindgen/emitter.rb

Instance Method Summary collapse

Instance Method Details

#aggregate_field_name(field, aggregate_node:) ⇒ Object



202
203
204
205
206
207
208
209
210
211
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 202

def aggregate_field_name(field, aggregate_node:)
  name = field["name"]
  return name if name && !name.empty?

  anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
  return name unless anonymous_record

  field_index = Array(aggregate_node["inner"]).select { |child| child["kind"] == "FieldDecl" }.index { |child| child["id"] == field["id"] } || 0
  "anonymous_#{anonymous_record.fetch("tagUsed")}_#{field_index}"
end

#aggregate_field_type(field, owner_name:, aggregate_node:) ⇒ Object



192
193
194
195
196
197
198
199
200
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 192

def aggregate_field_type(field, owner_name:, aggregate_node:)
  override = field_type_override(owner_name, field["name"])
  return override if override

  anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
  return synthetic_aggregate_name(owner_name, field, aggregate_node) if anonymous_record && !field_unnamed?(field)

  map_type_node(field, context: "field #{owner_name}.#{field["name"]}")
end

#anonymous_record_decl_for_field(field, aggregate_node) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 217

def anonymous_record_decl_for_field(field, aggregate_node)
  qual_type = type_qual_type(field)
  return unless qual_type

  tag_match = qual_type.match(/\A(struct|union)\b/)
  return unless tag_match
  return unless qual_type.include?("(unnamed at ") || qual_type.include?("(anonymous at ")

  field_begin = source_location_key(field.dig("range", "begin"))
  return unless field_begin

  expected_tag = tag_match[1]
  Array(aggregate_node["inner"]).find do |child|
    next false unless child["kind"] == "RecordDecl"
    next false unless child["tagUsed"] == expected_tag

    source_location_key(child["loc"]) == field_begin
  end
end

#bindgen_param_name(name) ⇒ Object



110
111
112
113
114
115
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 110

def bindgen_param_name(name)
  name = emitted_name(name)
  return [name, nil] unless generated_binding_name_conflict?(name)

  ["#{name}_", nil]
end

#discover_synthetic_aggregate_dependencies(declarations) ⇒ Object



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
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 121

def discover_synthetic_aggregate_dependencies(declarations)
  pending = declarations.select { |declaration| %w[struct union].include?(declaration[:kind]) }.map do |declaration|
    [declaration[:name], declaration[:node]]
  end
  seen = {}

  until pending.empty?
    owner_name, aggregate_node = pending.shift
    key = [owner_name, aggregate_node["id"]]
    next if seen[key]

    seen[key] = true

    Array(aggregate_node["inner"]).select { |child| child["kind"] == "FieldDecl" }.each do |field|
      anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
      next unless anonymous_record

      if field_unnamed?(field)
        # Anonymous member: flattened into the parent. Keep walking so
        # named nested records still get synthesized.
        pending << [owner_name, anonymous_record]
        next
      end

      synthetic_name = synthetic_aggregate_name(owner_name, field, aggregate_node)
      unless @synthetic_declarations.any? { |declaration| declaration[:name] == synthetic_name }
        @synthetic_declarations << { kind: anonymous_record.fetch("tagUsed"), name: synthetic_name, node: anonymous_record }
        @aggregate_declarations[synthetic_name] = anonymous_record
      end

      pending << [synthetic_name, anonymous_record]
    end
  end
end

#discover_synthetic_field_type_dependencies(declarations) ⇒ Object



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
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 156

def discover_synthetic_field_type_dependencies(declarations)
  pending = declarations.select { |declaration| %w[struct union].include?(declaration[:kind]) }.map do |declaration|
    [declaration[:name], declaration[:node]]
  end
  pending.concat(
    @synthetic_declarations.select { |declaration| %w[struct union].include?(declaration[:kind]) }.map do |declaration|
      [declaration[:name], declaration[:node]]
    end,
  )
  seen = {}

  until pending.empty?
    owner_name, aggregate_node = pending.shift
    key = [owner_name, aggregate_node["id"]]
    next if seen[key]

    seen[key] = true

    Array(aggregate_node["inner"]).select { |child| child["kind"] == "FieldDecl" }.each do |field|
      anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
      if anonymous_record
        if field_unnamed?(field)
          # Flattened anonymous member: walk its fields under the owner.
          pending << [owner_name, anonymous_record]
          next
        end

        pending << [synthetic_aggregate_name(owner_name, field, aggregate_node), anonymous_record]
        next
      end

      aggregate_field_type(field, owner_name:, aggregate_node:)
    end
  end
end

#emit_aggregate_declaration(kind, name, node) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 69

def emit_aggregate_declaration(kind, name, node)
  explicit_c_name = aggregate_explicit_c_name(name, node)
  stripped = emitted_name(name)
  header = "#{kind} #{stripped}"
  header += " = c#{explicit_c_name.inspect}" if explicit_c_name
  header += ":"
  lines = [header]
  fields = Array(node["inner"]).select { |child| child["kind"] == "FieldDecl" }
  fields.each do |field|
    anonymous_record = anonymous_record_decl_for_field(field, node)
    if anonymous_record && field_unnamed?(field)
      lines.concat(emit_flattened_anonymous_record(anonymous_record, owner_name: name))
      next
    end
    field_type = aggregate_field_type(field, owner_name: name, aggregate_node: node)
    mt_name = emitted_name(aggregate_field_name(field, aggregate_node: node))
    lines << "    #{mt_name}: #{field_type}"
  end
  lines
end

#emit_declaration(declaration) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 47

def emit_declaration(declaration)
  case declaration[:kind]
  when "struct", "union"
    emit_aggregate_declaration(declaration[:kind], declaration[:name], declaration[:node])
  when "opaque"
    name = emitted_name(declaration[:name])
    line = "opaque #{name}"
    line += " = c#{declaration[:linkage_name].inspect}" if declaration[:linkage_name]
    [line]
  when "enum", "flags"
    emit_enum_declaration(declaration[:kind], emitted_name(declaration[:name]), declaration[:node])
  when "type_alias"
    ["type #{emitted_name(declaration[:name])} = #{declaration[:mapped_type]}"]
  when "const"
    ["const #{emitted_name(declaration[:name])}: #{declaration[:type]} = #{declaration[:value]}"]
  when "function"
    emit_function_declaration(declaration)
  else
    raise BindgenError, "unsupported bindgen declaration kind #{declaration[:kind]}"
  end
end

#emit_enum_declaration(kind, name, node) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 247

def emit_enum_declaration(kind, name, node)
  members = enum_member_values(node)
  backing_type = if members.empty?
                   "int"
                 else
                   map_c_type(members.first.dig(:node, "type", "qualType"), context: "enum #{name}")
                 end
  lines = ["#{kind} #{name}: #{backing_type}"]
  members.each do |member|
    lines << "    #{member[:node]["name"]} = #{member[:value]}"
  end
  lines
end

#emit_flattened_anonymous_record(record, owner_name:) ⇒ Object

Anonymous union/struct members are flattened into the owning aggregate, matching C semantics where their fields are accessible directly on the enclosing type (e.g. b3ChildShape.hull, b3TreeNode.children).



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 93

def emit_flattened_anonymous_record(record, owner_name:)
  Array(record["inner"]).select { |child| child["kind"] == "FieldDecl" }.flat_map do |field|
    anonymous_record = anonymous_record_decl_for_field(field, record)
    if anonymous_record && field_unnamed?(field)
      emit_flattened_anonymous_record(anonymous_record, owner_name:)
    else
      field_type = aggregate_field_type(field, owner_name:, aggregate_node: record)
      ["    #{emitted_name(field["name"])}: #{field_type}"]
    end
  end
end

#emit_float_value(value, expected_type) ⇒ Object



348
349
350
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 348

def emit_float_value(value, expected_type)
  value.match?(/[.eE]/) ? value : "#{value}.0"
end

#emit_function_declaration(declaration) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 282

def emit_function_declaration(declaration)
  original_name = declaration[:name]
  stripped_name = emitted_name(original_name)
  params = declaration[:params].map do |param|
    param_name, = bindgen_param_name(param[:name])
    "#{param_name}: #{param[:type]}"
  end
  params << "..." if declaration[:variadic]
  line = "external function #{stripped_name}(#{params.join(', ')}) -> #{declaration[:return_type]}"
  if stripped_name != original_name
    line += " = c#{original_name.inspect}"
  end
  [line]
end

#emit_module(declarations) ⇒ Object



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
40
41
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 13

def emit_module(declarations)
  declarations = synthetic_declarations_for(declarations) + declarations

  lines = []
  lines << "# generated by mtc bindgen from #{@header_path}"
  lines << "external"
  lines << ""

  @module_imports.each do |import|
    lines << %(import #{import.fetch(:module_name)} as #{import.fetch(:alias)})
  end

  directives = []
  @link_libraries.each do |library|
    directives << %(link #{library.dump})
  end
  includes = @include_directives && !@include_directives.empty? ? @include_directives : [File.basename(@header_path)]
  includes.each do |include_name|
    directives << %(include #{include_name.dump})
  end
  lines.concat(directives)

  declarations.each do |declaration|
    lines.concat(emit_declaration(declaration))
  end

  source = lines.join("\n") + "\n"
  Formatter.format_source(source, path: generated_module_path, mode: :tidy)
end

#emitted_name(raw_name) ⇒ Object



7
8
9
10
11
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 7

def emitted_name(raw_name)
  return raw_name unless @strip_leading_underscores

  raw_name.sub(/\A_+/, "")
end

#enum_member_values(node) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 261

def enum_member_values(node)
  next_value = 0

  Array(node["inner"]).filter_map do |child|
    next unless child["kind"] == "EnumConstantDecl"

    explicit_value = integer_value(child)
    value = explicit_value ? Integer(explicit_value, 10) : next_value
    next_value = value + 1
    { node: child, value: }
  end
end

#field_unnamed?(field) ⇒ Boolean

Returns:

  • (Boolean)


105
106
107
108
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 105

def field_unnamed?(field)
  name = field["name"]
  name.nil? || name.empty?
end

#generated_binding_name_conflict?(name) ⇒ Boolean

Returns:

  • (Boolean)


117
118
119
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 117

def generated_binding_name_conflict?(name)
  Token::KEYWORDS.key?(name) || Types::RESERVED_VALUE_TYPE_NAMES.include?(name)
end

#generated_module_pathObject



43
44
45
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 43

def generated_module_path
  "#{@module_name.tr('.', '/')}" + ".mt"
end

#integer_value(node) ⇒ Object



274
275
276
277
278
279
280
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 274

def integer_value(node)
  constant = Array(node["inner"]).find { |child| child["kind"] == "ConstantExpr" }
  return constant["value"] if constant && constant.key?("value")

  literal = Array(node["inner"]).find { |child| child["kind"] == "IntegerLiteral" }
  literal&.[]("value")
end

#lower_aggregate_init_list(values, aggregate:, expected_type:, context:) ⇒ Object

Raises:



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 352

def lower_aggregate_init_list(values, aggregate:, expected_type:, context:)
  fields = Array(aggregate["inner"]).select { |child| child["kind"] == "FieldDecl" }
  raise BindgenError, "initializer field count mismatch for #{context}" if values.length > fields.length

  arguments = fields.each_with_index.map do |field, index|
    field_type = map_type_node(field, context: "field #{expected_type}.#{field["name"]}")
    value = values[index]
    lowered = if value
                lower_constant_expression(value, expected_type: field_type, context: "field #{field["name"]} of #{context}")
              else
                lower_zero_value(expected_type: field_type, context: "field #{field["name"]} of #{context}")
              end
    "#{field["name"]} = #{lowered}"
  end
  "#{expected_type}(#{arguments.join(', ')})"
end

#lower_constant_expression(node, expected_type:, context:) ⇒ Object

Raises:



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
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 297

def lower_constant_expression(node, expected_type:, context:)
  raise BindgenError, "missing initializer for #{context}" unless node

  case node["kind"]
  when "IntegerLiteral"
    integer_value = node.fetch("value")
    typed_null = pointer_zero_literal(expected_type, integer_value)
    return typed_null if typed_null

    integer_value
  when "FloatingLiteral"
    emit_float_value(node.fetch("value"), expected_type)
  when "StringLiteral"
    %(c#{node.fetch("value")})
  when "ImplicitValueInitExpr"
    lower_zero_value(expected_type:, context:)
  when "ImplicitCastExpr", "ConstantExpr", "CompoundLiteralExpr", "ParenExpr"
    child = Array(node["inner"]).first
    lower_constant_expression(child, expected_type:, context:)
  when "UnaryOperator"
    operator = node["opcode"]
    operand = lower_constant_expression(Array(node["inner"]).first, expected_type:, context:)
    "#{operator}#{operand}"
  when "InitListExpr"
    lower_init_list_expression(node, expected_type:, context:)
  else
    raise BindgenError, "unsupported constant initializer #{node["kind"]} for #{context}"
  end
end

#lower_init_list_expression(node, expected_type:, context:) ⇒ Object

Raises:



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 327

def lower_init_list_expression(node, expected_type:, context:)
  values = Array(node["inner"])

  aggregate = @aggregate_declarations[expected_type]
  return lower_aggregate_init_list(values, aggregate:, expected_type:, context:) if aggregate

  element_type, length = parse_array_type(expected_type)
  raise BindgenError, "unsupported aggregate constant type #{expected_type} for #{context}" unless element_type
  raise BindgenError, "initializer field count mismatch for #{context}" if values.length > length

  arguments = (0...length).map do |index|
    value = values[index]
    if value
      lower_constant_expression(value, expected_type: element_type, context: "element #{index} of #{context}")
    else
      lower_zero_value(expected_type: element_type, context: "element #{index} of #{context}")
    end
  end
  "#{expected_type}(#{arguments.join(', ')})"
end

#lower_zero_value(expected_type:, context:) ⇒ Object



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 369

def lower_zero_value(expected_type:, context:)
  return "false" if expected_type == "bool"
  return "0" if %w[char byte ubyte short ushort int uint long ulong ptr_int ptr_uint].include?(expected_type)
  return "0.0" if %w[float double].include?(expected_type)
  return "null[ptr[char]]" if expected_type == "cstr"
  return "null" if expected_type == "cstr?"
  return "null[#{expected_type}]" if expected_type.start_with?("ptr[") || expected_type.start_with?("const_ptr[")

  if @aggregate_declarations.key?(expected_type)
    return lower_aggregate_init_list([], aggregate: @aggregate_declarations.fetch(expected_type), expected_type:, context:)
  end

  element_type, length = parse_array_type(expected_type)
  if element_type
    values = Array.new(length) do |index|
      lower_zero_value(expected_type: element_type, context: "element #{index} of #{context}")
    end
    return "#{expected_type}(#{values.join(', ')})"
  end

  "#{expected_type}<-0"
end

#parse_array_type(type) ⇒ Object



404
405
406
407
408
409
410
411
412
413
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 404

def parse_array_type(type)
  return [nil, nil] unless type.start_with?("array[") && type.end_with?("]")

  parts = split_top_level_csv(type.delete_prefix("array[").delete_suffix("]"))
  return [nil, nil] unless parts.length == 2

  [parts[0], Integer(parts[1], 10)]
rescue ArgumentError
  [nil, nil]
end

#pointer_zero_literal(expected_type, value) ⇒ Object



392
393
394
395
396
397
398
399
400
401
402
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 392

def pointer_zero_literal(expected_type, value)
  return nil unless Integer(value, 10).zero?

  return "null[ptr[char]]" if expected_type == "cstr"
  return "null" if expected_type == "cstr?"
  return "null[#{expected_type}]" if expected_type.start_with?("ptr[") || expected_type.start_with?("const_ptr[")

  nil
rescue ArgumentError
  nil
end

#source_location_key(location) ⇒ Object



237
238
239
240
241
242
243
244
245
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 237

def source_location_key(location)
  return unless location

  [
    location["offset"],
    location.dig("includedFrom", "file"),
    location["file"],
  ]
end

#synthetic_aggregate_name(owner_name, field, aggregate_node) ⇒ Object



213
214
215
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 213

def synthetic_aggregate_name(owner_name, field, aggregate_node)
  "#{owner_name}_#{aggregate_field_name(field, aggregate_node:)}"
end