mt-lang 0.3.41 → 0.3.42

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91b5015b218da9925b56895b551063fca825ca776fc336fe898ef17e332aae4e
4
- data.tar.gz: 46e4a2da80edfb7c64fc847ee10844c9f5d36572df35105ab6ed51e2455d347a
3
+ metadata.gz: 927d840392dcd8afb43392ab7ef3f0b620ff267a13107c1f539ad17596364c6c
4
+ data.tar.gz: f9f09a4989def57fbb058b3cc1ceeea62a23d1bfcec0c6bfe030c58e4ea26e8e
5
5
  SHA512:
6
- metadata.gz: 0c1dcedaf98ef374c9dd21cded75f8512c755a63e4a3a155e494d9edf098b2d53da3b258edab5d63066a2a9eaa3c2a3883aa69241608509085caf398c1edecc1
7
- data.tar.gz: a427c7ad0f2475b7dc5cccadcb6cc143ab195f906b7b9b13407a7e754043fd8550495c7dc750f5f27c92e2eca13477d9f6697650bddd515403526812b1dac06b
6
+ metadata.gz: a7d0a57ff106227cc3e59d3ac1b425ad8cc0deecf4f0f9016f09984074cd624f43ddc7e178935c4af31a472cdd79eff6275c18a410030d678c0e1a0567666343
7
+ data.tar.gz: 53edd11a322c1145dbab6d63209dd321e720a22c0f269b08f84be31638fac77f7bb092aa5920611cb12603905eb2260612f49e325d0644ebc35bb95295b134c1
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.3.41"
6
+ VERSION = "0.3.42"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -280,11 +280,28 @@ module MilkTea
280
280
  qual_type = node.dig("type", "qualType")
281
281
  if macro_probe_declaration?(node) && qual_type.to_s.include?("typeof")
282
282
  node.dig("type", "desugaredQualType") || qual_type
283
+ elsif constant_typedef_alias_aggregate?(node)
284
+ node.dig("type", "desugaredQualType") || qual_type
283
285
  else
284
286
  qual_type
285
287
  end
286
288
  end
287
289
 
290
+ # A constant whose spelled type is a typedef alias that resolves to an
291
+ # aggregate (e.g. `typedef b3Vec3 b3Pos` followed by
292
+ # `static const b3Pos b3Pos_zero`) must lower against the underlying
293
+ # record, not the alias name, so init-list emission can find the
294
+ # aggregate declaration.
295
+ def constant_typedef_alias_aggregate?(node)
296
+ qual_type = node.dig("type", "qualType").to_s
297
+ spelled = strip_qualifiers(qual_type)
298
+ return false unless spelled.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
299
+ return false if @aggregate_declarations.key?(spelled)
300
+
301
+ desugared = strip_qualifiers(node.dig("type", "desugaredQualType").to_s)
302
+ !desugared.empty? && desugared != spelled && desugared.match?(/\A(?:struct|union)\b/)
303
+ end
304
+
288
305
  def function_return_type(node)
289
306
  qual_type = type_qual_type(node)
290
307
  match = qual_type&.match(/\A(.+?)\s*\((?:.*)\)\z/)
@@ -75,6 +75,11 @@ module MilkTea
75
75
  lines = [header]
76
76
  fields = Array(node["inner"]).select { |child| child["kind"] == "FieldDecl" }
77
77
  fields.each do |field|
78
+ anonymous_record = anonymous_record_decl_for_field(field, node)
79
+ if anonymous_record && field_unnamed?(field)
80
+ lines.concat(emit_flattened_anonymous_record(anonymous_record, owner_name: name))
81
+ next
82
+ end
78
83
  field_type = aggregate_field_type(field, owner_name: name, aggregate_node: node)
79
84
  mt_name = emitted_name(aggregate_field_name(field, aggregate_node: node))
80
85
  lines << " #{mt_name}: #{field_type}"
@@ -82,6 +87,26 @@ module MilkTea
82
87
  lines
83
88
  end
84
89
 
90
+ # Anonymous union/struct members are flattened into the owning aggregate,
91
+ # matching C semantics where their fields are accessible directly on the
92
+ # enclosing type (e.g. `b3ChildShape.hull`, `b3TreeNode.children`).
93
+ def emit_flattened_anonymous_record(record, owner_name:)
94
+ Array(record["inner"]).select { |child| child["kind"] == "FieldDecl" }.flat_map do |field|
95
+ anonymous_record = anonymous_record_decl_for_field(field, record)
96
+ if anonymous_record && field_unnamed?(field)
97
+ emit_flattened_anonymous_record(anonymous_record, owner_name:)
98
+ else
99
+ field_type = aggregate_field_type(field, owner_name:, aggregate_node: record)
100
+ [" #{emitted_name(field["name"])}: #{field_type}"]
101
+ end
102
+ end
103
+ end
104
+
105
+ def field_unnamed?(field)
106
+ name = field["name"]
107
+ name.nil? || name.empty?
108
+ end
109
+
85
110
  def bindgen_param_name(name)
86
111
  name = emitted_name(name)
87
112
  return [name, nil] unless generated_binding_name_conflict?(name)
@@ -110,6 +135,13 @@ module MilkTea
110
135
  anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
111
136
  next unless anonymous_record
112
137
 
138
+ if field_unnamed?(field)
139
+ # Anonymous member: flattened into the parent. Keep walking so
140
+ # named nested records still get synthesized.
141
+ pending << [owner_name, anonymous_record]
142
+ next
143
+ end
144
+
113
145
  synthetic_name = synthetic_aggregate_name(owner_name, field, aggregate_node)
114
146
  unless @synthetic_declarations.any? { |declaration| declaration[:name] == synthetic_name }
115
147
  @synthetic_declarations << { kind: anonymous_record.fetch("tagUsed"), name: synthetic_name, node: anonymous_record }
@@ -140,12 +172,19 @@ module MilkTea
140
172
  seen[key] = true
141
173
 
142
174
  Array(aggregate_node["inner"]).select { |child| child["kind"] == "FieldDecl" }.each do |field|
143
- aggregate_field_type(field, owner_name:, aggregate_node:)
144
-
145
175
  anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
146
- next unless anonymous_record
176
+ if anonymous_record
177
+ if field_unnamed?(field)
178
+ # Flattened anonymous member: walk its fields under the owner.
179
+ pending << [owner_name, anonymous_record]
180
+ next
181
+ end
182
+
183
+ pending << [synthetic_aggregate_name(owner_name, field, aggregate_node), anonymous_record]
184
+ next
185
+ end
147
186
 
148
- pending << [synthetic_aggregate_name(owner_name, field, aggregate_node), anonymous_record]
187
+ aggregate_field_type(field, owner_name:, aggregate_node:)
149
188
  end
150
189
  end
151
190
  end
@@ -155,7 +194,7 @@ module MilkTea
155
194
  return override if override
156
195
 
157
196
  anonymous_record = anonymous_record_decl_for_field(field, aggregate_node)
158
- return synthetic_aggregate_name(owner_name, field, aggregate_node) if anonymous_record
197
+ return synthetic_aggregate_name(owner_name, field, aggregate_node) if anonymous_record && !field_unnamed?(field)
159
198
 
160
199
  map_type_node(field, context: "field #{owner_name}.#{field["name"]}")
161
200
  end
@@ -60,6 +60,13 @@ module MilkTea
60
60
  raw_module_name: "std.c.box2d",
61
61
  policy_path: root.join("bindings/imported/box2d.binding.json"),
62
62
  ),
63
+ Binding.new(
64
+ name: "box3d",
65
+ module_name: "std.box3d",
66
+ binding_path: root.join("std/box3d.mt"),
67
+ raw_module_name: "std.c.box3d",
68
+ policy_path: root.join("bindings/imported/box3d.binding.json"),
69
+ ),
63
70
  Binding.new(
64
71
  name: "cjson",
65
72
  module_name: "std.cjson",
@@ -11,6 +11,8 @@ module MilkTea
11
11
  vendored_glfw_library = vendored_glfw.library(root:)
12
12
  vendored_box2d = MilkTea::VendoredBox2D
13
13
  vendored_box2d_library = vendored_box2d.library(root:)
14
+ vendored_box3d = MilkTea::VendoredBox3D
15
+ vendored_box3d_library = vendored_box3d.library(root:)
14
16
  vendored_cjson = MilkTea::VendoredCJSON
15
17
  vendored_cjson_library = vendored_cjson.library(root:)
16
18
  vendored_flecs = MilkTea::VendoredFlecs
@@ -960,6 +962,26 @@ module MilkTea
960
962
  vendored_box2d.header_root(root:).join("box2d.h").to_s,
961
963
  ],
962
964
  ),
965
+ Binding.new(
966
+ name: "box3d",
967
+ module_name: "std.c.box3d",
968
+ binding_path: root.join("std/c/box3d.mt"),
969
+ include_directives: ["box3d/box3d.h"],
970
+ link_libraries: ["box3d"],
971
+ vendored_library: vendored_box3d_library,
972
+ clang_args: vendored_box3d.include_flags(root:),
973
+ compiler_flags: vendored_box3d.include_flags(root:),
974
+ tracked_header_paths: [
975
+ vendored_box3d.header_root(root:).join("box3d.h").to_s,
976
+ ],
977
+ tracked_header_prefixes: [
978
+ vendored_box3d.header_root(root:).to_s,
979
+ ],
980
+ declaration_name_prefixes: ["b3", "B3_"],
981
+ header_candidates: [
982
+ vendored_box3d.header_root(root:).join("box3d.h").to_s,
983
+ ],
984
+ ),
963
985
  Binding.new(
964
986
  name: "cjson",
965
987
  module_name: "std.c.cjson",
@@ -195,6 +195,15 @@ module MilkTea
195
195
  include/box2d/box2d.h
196
196
  ],
197
197
  ),
198
+ Source.new(
199
+ name: "box3d",
200
+ checkout_root: data.join("third_party/box3d-upstream"),
201
+ repository_url: "https://github.com/erincatto/box3d.git",
202
+ revision: "30c67b5e6d0a3a66f0f506c69ce9e9e0587e3b7c",
203
+ sentinel_paths: %w[
204
+ include/box3d/box3d.h
205
+ ],
206
+ ),
198
207
  Source.new(
199
208
  name: "cjson",
200
209
  checkout_root: data.join("third_party/cjson-upstream"),
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vendored_c_library"
4
+
5
+ module MilkTea
6
+ module VendoredBox3D
7
+ Error = VendoredCLibrary::Error
8
+
9
+ CONFIGURE_ARGS = %w[
10
+ -DCMAKE_BUILD_TYPE=Release
11
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON
12
+ -DBUILD_SHARED_LIBS=OFF
13
+ -DBOX3D_SAMPLES=OFF
14
+ -DBOX3D_BENCHMARKS=OFF
15
+ -DBOX3D_DOCS=OFF
16
+ -DBOX3D_PROFILE=OFF
17
+ -DBOX3D_VALIDATE=OFF
18
+ -DBOX3D_UNIT_TESTS=OFF
19
+ ].freeze
20
+
21
+ SYSTEM_LINK_FLAGS = %w[
22
+ -lm
23
+ ].freeze
24
+
25
+ def self.library(root: MilkTea.root)
26
+ resolved_root = Pathname.new(File.expand_path(root.to_s))
27
+ @libraries ||= {}
28
+ @libraries[resolved_root.to_s] ||= VendoredCLibrary::CMake.new(
29
+ name: "box3d",
30
+ source_root: source_root(root: resolved_root),
31
+ build_root: build_root(root: resolved_root),
32
+ install_root: install_root(root: resolved_root),
33
+ archive_path: archive_path(root: resolved_root),
34
+ include_roots: [include_root(root: resolved_root)],
35
+ configure_args: CONFIGURE_ARGS,
36
+ system_link_flags: SYSTEM_LINK_FLAGS,
37
+ cc_env_var: "BOX3D_CC",
38
+ )
39
+ end
40
+
41
+ def self.source_root(root: MilkTea.root)
42
+ MilkTea.writable_root_for(root).join("third_party/box3d-upstream")
43
+ end
44
+
45
+ def self.include_root(root: MilkTea.root)
46
+ source_root(root:).join("include")
47
+ end
48
+
49
+ def self.header_root(root: MilkTea.root)
50
+ include_root(root:).join("box3d")
51
+ end
52
+
53
+ def self.build_root(root: MilkTea.root)
54
+ MilkTea.writable_root_for(root).join("tmp/vendored-box3d")
55
+ end
56
+
57
+ def self.install_root(root: MilkTea.root)
58
+ MilkTea.writable_root_for(root).join("tmp/vendored-box3d-prefix")
59
+ end
60
+
61
+ def self.archive_path(root: MilkTea.root)
62
+ install_root(root:).join("lib/libbox3d.a")
63
+ end
64
+
65
+ def self.include_flags(root: MilkTea.root)
66
+ library(root:).include_flags
67
+ end
68
+
69
+ def self.link_flags(root: MilkTea.root)
70
+ library(root:).link_flags
71
+ end
72
+
73
+ def self.prepare!(root: MilkTea.root, **kwargs)
74
+ library(root:).prepare!(**kwargs)
75
+ end
76
+ end
77
+ end
@@ -11,6 +11,7 @@ require_relative "bindings/vendored_raylib"
11
11
  require_relative "bindings/vendored_sdl3"
12
12
  require_relative "bindings/vendored_glfw"
13
13
  require_relative "bindings/vendored_box2d"
14
+ require_relative "bindings/vendored_box3d"
14
15
  require_relative "bindings/vendored_cjson"
15
16
  require_relative "bindings/vendored_flecs"
16
17
  require_relative "bindings/vendored_libuv"
@@ -17,7 +17,7 @@ module MilkTea
17
17
  if (alias_name = checked_index_alias(expression))
18
18
  "(*#{alias_name})"
19
19
  else
20
- "(*#{checked_array_index_helper_name(expression.receiver_type)}(#{emit_address_of_operand(expression.receiver)}, #{emit_expression(expression.index)}))"
20
+ "(*#{checked_array_index_helper_name(expression.receiver_type)}(#{emit_checked_array_index_argument(expression.receiver)}, #{emit_expression(expression.index)}))"
21
21
  end
22
22
  when IR::CheckedSpanIndex
23
23
  if (alias_name = checked_index_alias(expression))
@@ -26,7 +26,7 @@ module MilkTea
26
26
  "(*#{checked_span_index_helper_name(expression.receiver_type)}(#{emit_expression(expression.receiver)}, #{emit_expression(expression.index)}))"
27
27
  end
28
28
  when IR::NullableIndex
29
- "#{nullable_array_index_helper_name(expression.receiver_type)}(#{emit_address_of_operand(expression.receiver)}, #{emit_expression(expression.index)})"
29
+ "#{nullable_array_index_helper_name(expression.receiver_type)}(#{emit_checked_array_index_argument(expression.receiver)}, #{emit_expression(expression.index)})"
30
30
  when IR::NullableSpanIndex
31
31
  "#{nullable_span_index_helper_name(expression.receiver_type)}(#{emit_expression(expression.receiver)}, #{emit_expression(expression.index)})"
32
32
  when IR::Call
@@ -81,7 +81,7 @@ module MilkTea
81
81
  case expression.expression
82
82
  when IR::CheckedIndex
83
83
  alias_name = checked_index_alias(expression.expression)
84
- alias_name || "#{checked_array_index_helper_name(expression.expression.receiver_type)}(#{emit_address_of_operand(expression.expression.receiver)}, #{emit_expression(expression.expression.index)})"
84
+ alias_name || "#{checked_array_index_helper_name(expression.expression.receiver_type)}(#{emit_checked_array_index_argument(expression.expression.receiver)}, #{emit_expression(expression.expression.index)})"
85
85
  when IR::CheckedSpanIndex
86
86
  alias_name = checked_index_alias(expression.expression)
87
87
  alias_name || "#{checked_span_index_helper_name(expression.expression.receiver_type)}(#{emit_expression(expression.expression.receiver)}, #{emit_expression(expression.expression.index)})"
@@ -344,6 +344,16 @@ module MilkTea
344
344
  "(void)#{wrap_expression(expression)}"
345
345
  end
346
346
 
347
+ def emit_checked_array_index_argument(receiver)
348
+ if receiver.is_a?(IR::Unary) && receiver.operator == "*"
349
+ emit_expression(receiver.operand)
350
+ elsif c_expression_lvalue?(receiver)
351
+ emit_expression(receiver)
352
+ else
353
+ "&(#{c_type(receiver.type)}[1]){ #{emit_expression(receiver)} }[0]"
354
+ end
355
+ end
356
+
347
357
  def emit_address_of_operand(expression)
348
358
  return emit_expression(expression.operand) if expression.is_a?(IR::Unary) && expression.operator == "*"
349
359
 
@@ -490,8 +500,14 @@ module MilkTea
490
500
  def emit_cyclic_array_initializer(field_type, value)
491
501
  elem_c_name = c_type(array_element_type(field_type))
492
502
  elem_count = array_length(field_type)
493
- elements = value.is_a?(IR::ArrayLiteral) ? value.elements.map { |e| emit_initializer(e) }.join(", ") : ""
494
- "((#{elem_c_name}*)memcpy(malloc(#{elem_count} * sizeof(#{elem_c_name})), &(#{elem_c_name}[#{elem_count}]){ #{elements} }, #{elem_count} * sizeof(#{elem_c_name})))"
503
+ source_expr = if value.is_a?(IR::ArrayLiteral)
504
+ "&(#{elem_c_name}[#{elem_count}]){ #{value.elements.map { |e| emit_initializer(e) }.join(", ")} }"
505
+ elsif c_expression_lvalue?(value)
506
+ "&(#{emit_expression(value)})"
507
+ else
508
+ "&(#{elem_c_name}[#{elem_count}]){ #{emit_expression(value)} }"
509
+ end
510
+ "((#{elem_c_name}*)memcpy(malloc(#{elem_count} * sizeof(#{elem_c_name})), #{source_expr}, #{elem_count} * sizeof(#{elem_c_name})))"
495
511
  end
496
512
 
497
513
  def emit_cyclic_struct_initializer(field_type, value)
@@ -137,7 +137,7 @@ module MilkTea
137
137
 
138
138
  collect_checked_array_index_types.any? || collect_checked_span_index_types.any? ||
139
139
  uses_format_helpers? ||
140
- emitted_functions.any? { |function| function_uses_named_call?(function, %w[mt_fatal mt_str_buffer_len mt_str_buffer_as_cstr mt_str_buffer_assign mt_str_buffer_append mt_foreign_str_to_cstr_temp mt_foreign_strs_to_cstrs_temp]) }
140
+ emitted_functions.any? { |function| function_uses_named_call?(function, %w[mt_fatal mt_str_buffer_len mt_str_buffer_as_cstr mt_str_buffer_assign mt_str_buffer_append mt_foreign_str_to_cstr_temp mt_foreign_strs_to_cstrs_temp mt_str_concat]) }
141
141
  end
142
142
 
143
143
  def uses_mt_fatal_str_helper?
@@ -51,6 +51,7 @@ module MilkTea
51
51
  "",
52
52
  "static mt_str mt_str_concat(mt_str a, mt_str b) {",
53
53
  "#{INDENT}uintptr_t total = a.len + b.len;",
54
+ "#{INDENT}if (total > MT_STR_CONCAT_BUF_SIZE) mt_fatal(\"str concatenation exceeds the scratch buffer budget\");",
54
55
  "#{INDENT}if (mt_str_concat_offset + total > MT_STR_CONCAT_BUF_SIZE) {",
55
56
  "#{INDENT * 2}mt_str_concat_offset = 0;",
56
57
  "#{INDENT}}",
@@ -297,6 +298,9 @@ module MilkTea
297
298
  "#{INDENT}for (int t = 0; t < nworkers; t++) {",
298
299
  "#{INDENT * 2}uv_thread_join(&threads[t]);",
299
300
  "#{INDENT}}",
301
+ "#{INDENT}for (int t = nworkers + 1; t < count; t++) {",
302
+ "#{INDENT * 2}items[t].work(items[t].data);",
303
+ "#{INDENT}}",
300
304
  "}",
301
305
  ]
302
306
  end
@@ -590,11 +594,11 @@ module MilkTea
590
594
 
591
595
  def emit_checked_array_index_helper(type)
592
596
  helper_name = checked_array_index_helper_name(type)
593
- params = [c_declaration(type, '(*array)'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
597
+ params = [c_declaration(pointer_to(array_element_type(type)), 'array'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
594
598
  [
595
599
  "static inline #{c_function_declaration(pointer_to(array_element_type(type)), helper_name, params)} {",
596
600
  "#{INDENT}if (index >= #{array_length(type)}) mt_fatal(\"array index out of bounds\");",
597
- "#{INDENT}return &(*array)[index];",
601
+ "#{INDENT}return &array[index];",
598
602
  "}",
599
603
  ]
600
604
  end
@@ -612,11 +616,11 @@ module MilkTea
612
616
 
613
617
  def emit_nullable_array_index_helper(type)
614
618
  helper_name = nullable_array_index_helper_name(type)
615
- params = [c_declaration(type, '(*array)'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
619
+ params = [c_declaration(pointer_to(array_element_type(type)), 'array'), c_declaration(Types::Registry.primitive('ptr_uint'), 'index')].join(', ')
616
620
  [
617
621
  "static inline #{c_function_declaration(pointer_to(array_element_type(type)), helper_name, params)} {",
618
622
  "#{INDENT}if (index >= #{array_length(type)}) return NULL;",
619
- "#{INDENT}return &(*array)[index];",
623
+ "#{INDENT}return &array[index];",
620
624
  "}",
621
625
  ]
622
626
  end
@@ -495,7 +495,7 @@ module MilkTea
495
495
  def emit_checked_index_pointer(expression)
496
496
  case expression
497
497
  when IR::CheckedIndex
498
- "#{checked_array_index_helper_name(expression.receiver_type)}(#{emit_address_of_operand(expression.receiver)}, #{emit_expression(expression.index)})"
498
+ "#{checked_array_index_helper_name(expression.receiver_type)}(#{emit_checked_array_index_argument(expression.receiver)}, #{emit_expression(expression.index)})"
499
499
  when IR::CheckedSpanIndex
500
500
  "#{checked_span_index_helper_name(expression.receiver_type)}(#{emit_expression(expression.receiver)}, #{emit_expression(expression.index)})"
501
501
  else
@@ -223,6 +223,26 @@ module MilkTea
223
223
  soa_types = []
224
224
  visited = {}
225
225
 
226
+ all_emitted_top_level_values.each do |value|
227
+ collect_soa_type(value.type, soa_types, visited)
228
+ end
229
+
230
+ @program.structs.each do |struct_decl|
231
+ struct_decl.fields.each do |field|
232
+ collect_soa_type(field.type, soa_types, visited)
233
+ end
234
+ end
235
+
236
+ @program.unions.each do |union_decl|
237
+ union_decl.fields.each do |field|
238
+ collect_soa_type(field.type, soa_types, visited)
239
+ end
240
+ end
241
+
242
+ each_variant_arm_field_type do |field_type|
243
+ collect_soa_type(field_type, soa_types, visited)
244
+ end
245
+
226
246
  emitted_functions.each do |function|
227
247
  collect_soa_type(function.return_type, soa_types, visited)
228
248
  function.params.each do |param|
@@ -231,10 +251,9 @@ module MilkTea
231
251
  collect_soa_from_statements(function.body, soa_types, visited)
232
252
  end
233
253
 
234
- @program.structs.each do |struct_decl|
235
- struct_decl.fields.each do |field|
236
- collect_soa_type(field.type, soa_types, visited)
237
- end
254
+ @program.static_asserts.each do |statement|
255
+ collect_soa_type_in_expression(statement.condition, soa_types, visited)
256
+ collect_soa_type_in_expression(statement.message, soa_types, visited)
238
257
  end
239
258
 
240
259
  soa_types.uniq
@@ -636,13 +636,9 @@ module MilkTea
636
636
  active_defers: active_defers + local_defers,
637
637
  loop_flow: nested_loop_flow(loop_flow, local_defers),
638
638
  )
639
+ storage_ref = IR::Name.new(name: linkage_name, type: storage_type, pointer: false)
639
640
  lowered << IR::IfStmt.new(
640
- condition: IR::Binary.new(
641
- operator: "==",
642
- left: IR::Name.new(name: linkage_name, type: storage_type, pointer: false),
643
- right: IR::NullLiteral.new(type: storage_type),
644
- type: @ctx.types.fetch("bool"),
645
- ),
641
+ condition: let_else_failure_condition(storage_ref, storage_type),
646
642
  then_body: else_body,
647
643
  else_body: nil,
648
644
  )
@@ -421,7 +421,8 @@ module MilkTea
421
421
  body = [async_frame_cast_declaration(frame_type, async_info)]
422
422
 
423
423
  not_ready_expr = IR::Unary.new(operator: "not", operand: async_frame_field_expression(frame_expr, "ready", @ctx.types.fetch("bool")), type: @ctx.types.fetch("bool"))
424
- not_ready_return = [IR::ReturnStmt.new(value: nil)]
424
+ frame_free_stmt = IR::ExpressionStmt.new(expression: IR::Call.new(callee: "mt_async_free", arguments: [raw_frame_expr], type: @ctx.types.fetch("void")))
425
+ not_ready_return = [frame_free_stmt, IR::ReturnStmt.new(value: nil)]
425
426
 
426
427
  if async_info[:await_fields].any?
427
428
  await_release_stmts = []
@@ -589,11 +590,17 @@ module MilkTea
589
590
  )
590
591
  body << IR::ExpressionStmt.new(expression: ready_assign)
591
592
 
592
- waiter_frame_expr = async_frame_field_expression(frame_expr, "waiter_frame", async_info[:void_ptr])
593
+ waiter_frame_field = async_frame_field_expression(frame_expr, "waiter_frame", async_info[:void_ptr])
593
594
  wake_stmts = [
595
+ IR::LocalDecl.new(
596
+ name: "waiter_frame",
597
+ linkage_name: "__mt_waiter_frame",
598
+ type: async_info[:void_ptr],
599
+ value: waiter_frame_field,
600
+ ),
594
601
  IR::ExpressionStmt.new(
595
602
  expression: IR::Assignment.new(
596
- target: async_frame_field_expression(frame_expr, "waiter_frame", async_info[:void_ptr]),
603
+ target: waiter_frame_field,
597
604
  operator: "=",
598
605
  value: IR::NullLiteral.new(type: async_info[:void_ptr]),
599
606
  ),
@@ -601,13 +608,13 @@ module MilkTea
601
608
  IR::ExpressionStmt.new(
602
609
  expression: IR::Call.new(
603
610
  callee: IR::Name.new(name: async_frame_field_c_name("waiter"), type: async_info[:wake_type], pointer: false),
604
- arguments: [waiter_frame_expr],
611
+ arguments: [IR::Name.new(name: "__mt_waiter_frame", type: async_info[:void_ptr], pointer: false)],
605
612
  type: @ctx.types.fetch("void"),
606
613
  ),
607
614
  ),
608
615
  ]
609
616
  body << IR::IfStmt.new(
610
- condition: waiter_frame_expr,
617
+ condition: waiter_frame_field,
611
618
  then_body: wake_stmts,
612
619
  else_body: nil,
613
620
  )
@@ -364,6 +364,17 @@ module MilkTea
364
364
  end
365
365
  end
366
366
 
367
+ # Emits a type-name prefix that the semantic re-check of generated proc
368
+ # roots can actually resolve: bare names for prelude and current-module
369
+ # types, the qualified module path otherwise.
370
+ def ast_type_ref_base_parts(type)
371
+ return [type.name] if type.module_name.nil?
372
+ return [type.name] if type.module_name == @ctx.module_name
373
+ return [type.name] if %w[std.option std.result].include?(type.module_name)
374
+
375
+ type.module_name.split(".") + [type.name]
376
+ end
377
+
367
378
  def ast_type_ref_for(type)
368
379
  case type
369
380
  when Types::Primitive
@@ -392,9 +403,20 @@ module MilkTea
392
403
  when Types::TypeVar
393
404
  AST::TypeRef.new(name: AST::QualifiedName.new(parts: [type.name]), arguments: [], nullable: false)
394
405
  when Types::StructInstance
395
- base_parts = type.module_name ? type.module_name.split(".") + [type.name] : [type.name]
396
406
  AST::TypeRef.new(
397
- name: AST::QualifiedName.new(parts: base_parts),
407
+ name: AST::QualifiedName.new(parts: ast_type_ref_base_parts(type)),
408
+ arguments: type.arguments.map do |argument|
409
+ if argument.is_a?(Types::LiteralTypeArg)
410
+ AST::TypeArgument.new(value: AST::IntegerLiteral.new(lexeme: argument.value.to_s, value: argument.value))
411
+ else
412
+ AST::TypeArgument.new(value: ast_type_ref_for(argument))
413
+ end
414
+ end,
415
+ nullable: false,
416
+ )
417
+ when Types::VariantInstance
418
+ AST::TypeRef.new(
419
+ name: AST::QualifiedName.new(parts: ast_type_ref_base_parts(type)),
398
420
  arguments: type.arguments.map do |argument|
399
421
  if argument.is_a?(Types::LiteralTypeArg)
400
422
  AST::TypeArgument.new(value: AST::IntegerLiteral.new(lexeme: argument.value.to_s, value: argument.value))
@@ -404,9 +426,8 @@ module MilkTea
404
426
  end,
405
427
  nullable: false,
406
428
  )
407
- when Types::Struct, Types::Union, Types::Opaque, Types::Enum, Types::Flags
408
- parts = type.module_name ? type.module_name.split(".") + [type.name] : [type.name]
409
- AST::TypeRef.new(name: AST::QualifiedName.new(parts: parts), arguments: [], nullable: false)
429
+ when Types::Struct, Types::Union, Types::Opaque, Types::Enum, Types::Flags, Types::Variant
430
+ AST::TypeRef.new(name: AST::QualifiedName.new(parts: ast_type_ref_base_parts(type)), arguments: [], nullable: false)
410
431
  when Types::Function
411
432
  AST::FunctionType.new(
412
433
  params: type.params.each_with_index.map { |param, i| AST::Param.new(name: param.name || "p#{i}", type: ast_type_ref_for(param.type)) },
@@ -248,7 +248,13 @@ module MilkTea
248
248
  raise_sema_error("unsupported expression #{expression.class.name}")
249
249
  end
250
250
 
251
- @resolved_expr_types[@ctx.ast.node_ids[expression.object_id]] = type
251
+ # Specialized generic-instance bodies share the same AST nodes across
252
+ # substitutions, so a node-id keyed cache cannot be populated there:
253
+ # the last-checked instance would overwrite every other instance's
254
+ # types and leak wrong types to lowering, which reads this cache.
255
+ # Regular functions carry an empty (but non-nil) substitution hash, so
256
+ # only a non-empty hash identifies an instance body.
257
+ @resolved_expr_types[@ctx.ast.node_ids[expression.object_id]] = type unless @current_type_substitutions&.any?
252
258
  type
253
259
  end
254
260
  end
@@ -961,7 +967,7 @@ module MilkTea
961
967
  callable_kind = resolution.kind
962
968
  callable = resolution.value
963
969
  receiver = resolution.receiver
964
- @resolved_call_kinds[@ctx.ast.node_ids[expression.callee.object_id]] = callable_kind
970
+ @resolved_call_kinds[@ctx.ast.node_ids[expression.callee.object_id]] = callable_kind unless @current_type_substitutions&.any?
965
971
 
966
972
  case callable_kind
967
973
  when :function
@@ -695,7 +695,9 @@ module MilkTea
695
695
  offset = CompileTime::Layout.offset_of(type, binding.const_value.field_name)
696
696
  return unless offset
697
697
 
698
- @const_values[@ctx.ast.node_ids[expression.object_id]] = offset
698
+ # Same node-id key collision as resolved_expr_types: instance bodies
699
+ # share AST nodes across substitutions, so never cache their values.
700
+ @const_values[@ctx.ast.node_ids[expression.object_id]] = offset unless @current_type_substitutions&.any?
699
701
  end
700
702
 
701
703
  def infer_offsetof_type(type_ref, field_name, scopes: nil)
@@ -18,7 +18,7 @@ module MilkTea
18
18
  "Files & I/O" => %w[fs path stdio],
19
19
  "System" => %w[ctype errno process time c],
20
20
  "Network & HTTP" => %w[net http uri cookie curl],
21
- "Game & Graphics" => %w[raylib box2d flecs enet cgltf cjson],
21
+ "Game & Graphics" => %w[raylib box2d box3d flecs enet cgltf cjson],
22
22
  "Algorithms & AI" => %w[fsm behavior_tree goap],
23
23
  "Database & Matching" => %w[sqlite3 pcre2],
24
24
  "Utilities" => %w[cli terminal span spatial asset_pack],