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



170
171
172
173
174
175
176
177
178
179
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 170

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



160
161
162
163
164
165
166
167
168
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 160

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

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

#anonymous_record_decl_for_field(field, aggregate_node) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 185

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_field_name(name) ⇒ Object



85
86
87
88
89
90
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 85

def bindgen_field_name(name)
  name = emitted_name(name)
  return [name, nil] unless Token::KEYWORDS.key?(name)

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

#bindgen_param_name(name) ⇒ Object



92
93
94
95
96
97
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 92

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



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

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

      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



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

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|
      aggregate_field_type(field, owner_name:, aggregate_node:)

      anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
      next unless anonymous_record

      pending << [synthetic_aggregate_name(owner_name, field, aggregate_node), anonymous_record]
    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
# 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|
    field_type = aggregate_field_type(field, owner_name: name, aggregate_node: node)
    mt_name, = bindgen_field_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



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 215

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_float_value(value, expected_type) ⇒ Object



316
317
318
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 316

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

#emit_function_declaration(declaration) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 250

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



229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 229

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

#generated_binding_name_conflict?(name) ⇒ Boolean

Returns:

  • (Boolean)


99
100
101
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 99

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



242
243
244
245
246
247
248
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 242

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:



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 320

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:



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

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:



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 295

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



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 337

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



372
373
374
375
376
377
378
379
380
381
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 372

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



360
361
362
363
364
365
366
367
368
369
370
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 360

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



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

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



181
182
183
# File 'lib/milk_tea/bindings/bindgen/emitter.rb', line 181

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