mt-lang 0.4.23 → 0.4.25

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +8 -8
  5. data/lib/milk_tea/base.rb +1 -1
  6. data/lib/milk_tea/core/control_flow/builder.rb +12 -0
  7. data/lib/milk_tea/core/types/layout.rb +90 -19
  8. data/lib/milk_tea/tooling/cli/commands/std.rb +84 -0
  9. data/lib/milk_tea/tooling/cli.rb +25 -0
  10. data/lib/milk_tea/tooling/linter/fix_engine.rb +100 -0
  11. data/lib/milk_tea/tooling/linter/release_rules.rb +69 -11
  12. data/lib/milk_tea/tooling/linter/visitors.rb +58 -31
  13. data/lib/milk_tea/tooling/linter.rb +8 -1
  14. data/lib/milk_tea/tooling/std_catalog.rb +277 -0
  15. data/lib/milk_tea/tooling.rb +1 -0
  16. data/std/async/libuv_runtime.mt +20 -23
  17. data/std/async/mailbox.mt +2 -2
  18. data/std/base64.mt +3 -6
  19. data/std/behavior_tree.mt +1 -6
  20. data/std/binary.mt +2 -5
  21. data/std/color.mt +20 -20
  22. data/std/cookie.mt +1 -4
  23. data/std/deque.mt +2 -8
  24. data/std/encoding.mt +1 -4
  25. data/std/fmt.mt +1 -4
  26. data/std/graph.mt +5 -1
  27. data/std/htn.mt +9 -1
  28. data/std/http/server.mt +1 -4
  29. data/std/http.mt +14 -17
  30. data/std/jobs.mt +9 -10
  31. data/std/json.mt +17 -28
  32. data/std/linked_map.mt +3 -9
  33. data/std/map.mt +1 -4
  34. data/std/net/channel.mt +6 -6
  35. data/std/net/lobby.mt +3 -3
  36. data/std/net/manager.mt +0 -1
  37. data/std/net/mux.mt +1 -1
  38. data/std/net/packet.mt +4 -1
  39. data/std/net/punch.mt +3 -2
  40. data/std/net/rpc.mt +1 -3
  41. data/std/net/session.mt +12 -25
  42. data/std/net/sync.mt +3 -3
  43. data/std/net/turn.mt +7 -3
  44. data/std/net.mt +145 -159
  45. data/std/noise.mt +11 -7
  46. data/std/ordered_map.mt +2 -8
  47. data/std/ordered_set.mt +3 -5
  48. data/std/path.mt +1 -4
  49. data/std/process.mt +7 -18
  50. data/std/random.mt +2 -2
  51. data/std/sdl3/runtime.mt +1 -1
  52. data/std/steering.mt +27 -4
  53. data/std/string.mt +1 -4
  54. data/std/tar.mt +8 -10
  55. data/std/terminal.mt +21 -40
  56. data/std/thread.mt +3 -4
  57. data/std/tls.mt +22 -29
  58. data/std/toml.mt +23 -31
  59. data/std/uri.mt +2 -1
  60. data/std/utility.mt +2 -2
  61. data/std/vec.mt +8 -17
  62. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21ef46a88a5b23f5191412808ad2dfd353d68cead76553908ae5d13c0c773673
4
- data.tar.gz: 60cf1b226bb63cb9392d6c54c11c9aead667203d9d103811681aaa43b28c9596
3
+ metadata.gz: f4c65f190209c5486b52d39b2ac868b2e2c076ad3ec01206151df16675319cf9
4
+ data.tar.gz: 28bb0127501319fb64f0da085409bb8658b6fd9cd41d8c18c0ac2175608f93dd
5
5
  SHA512:
6
- metadata.gz: a9721db3d74e88c837e15a10657041010b1ff2aba40fee78ebf5c2862375d38121fcb3e3a57a328d3155314a39ba9fb7345b262fc7c99cc12bad41a98d70bc4c
7
- data.tar.gz: 4bf9aaa1e0b8a9ba9e5878ab9eece9266e9c386144309abc14f0c47ddc0111dc9de6796f38052e2a956386c159fe9e7c8a0833068e0d084707c133f8e893b9e5
6
+ metadata.gz: '098dc1e2c0303bbf39dcd4402d7f21482c682f83b904799c1c85a9a73207b4c8443c0a98bdee192db9d17a0bf33ec4a96ad31c03a733f28c57880b7601c91d42'
7
+ data.tar.gz: ce0f5ba1ae8e8c6f958ab4cd5e2d0e89c522a9f814d8e5e31a1e8b8b849faafe437caaab098bd3a4d59755cdbf4f12c23d521810e9892c6e039acf27176b7d10
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.3
1
+ 4.0.6
data/Gemfile CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- ruby "4.0.3"
5
+ ruby "4.0.6"
6
6
 
7
7
  gem "rake", "~> 13.4"
8
8
  gem "rexml", "~> 3.4"
data/Gemfile.lock CHANGED
@@ -5,7 +5,7 @@ GEM
5
5
  drb (2.2.3)
6
6
  fiddle (1.1.8)
7
7
  logger (1.7.0)
8
- minitest (6.0.4)
8
+ minitest (6.0.6)
9
9
  drb (~> 2.0)
10
10
  prism (~> 1.5)
11
11
  minitest-coverage (1.0.0.b3)
@@ -15,7 +15,7 @@ GEM
15
15
  puma (6.6.1)
16
16
  nio4r (~> 2.0)
17
17
  racc (1.8.1)
18
- rack (3.2.6)
18
+ rack (3.2.7)
19
19
  rack-protection (4.2.1)
20
20
  base64 (>= 0.1.0)
21
21
  logger (>= 1.6.0)
@@ -34,7 +34,7 @@ GEM
34
34
  rack-protection (= 4.2.1)
35
35
  rack-session (>= 2.0.0, < 3)
36
36
  tilt (~> 2.0)
37
- tilt (2.7.0)
37
+ tilt (2.9.0)
38
38
  tomlrb (2.0.4)
39
39
 
40
40
  PLATFORMS
@@ -58,25 +58,25 @@ CHECKSUMS
58
58
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
59
59
  fiddle (1.1.8) sha256=7fa8ee3627271497f3add5503acdbc3f40b32f610fc1cf49634f083ef3f32eee
60
60
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
61
- minitest (6.0.4) sha256=df1304664589d40f46089247fdc451f866b0ce0d7cae1457a15fc1eb7d48dca1
61
+ minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
62
62
  minitest-coverage (1.0.0.b3) sha256=27c8b6b8df1259325dd206baa2b143c594f8a88b7dfd97fd8d3389a0e8a6ad26
63
63
  mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198
64
64
  nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
65
65
  prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
66
66
  puma (6.6.1) sha256=b9b56e4a4ea75d1bfa6d9e1972ee2c9f43d0883f011826d914e8e37b3694ea1e
67
67
  racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
68
- rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
68
+ rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
69
69
  rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
70
70
  rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
71
71
  rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
72
72
  rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
73
73
  rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
74
74
  sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
75
- tilt (2.7.0) sha256=0d5b9ba69f6a36490c64b0eee9f6e9aad517e20dcc848800a06eb116f08c6ab3
75
+ tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5
76
76
  tomlrb (2.0.4) sha256=262f77947ac3ac9b3366a0a5940ecd238300c553e2e14f22009e2afcd2181b99
77
77
 
78
78
  RUBY VERSION
79
- ruby 4.0.3
79
+ ruby 4.0.6
80
80
 
81
81
  BUNDLED WITH
82
- 4.0.3
82
+ 4.0.16
data/lib/milk_tea/base.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module MilkTea
6
- VERSION = "0.4.23"
6
+ VERSION = "0.4.25"
7
7
 
8
8
  def self.root
9
9
  @root ||= Pathname.new(File.expand_path("../..", __dir__))
@@ -352,6 +352,12 @@ module MilkTea
352
352
  read_identifiers(expression.expression, names, reads_info)
353
353
  when AST::UnsafeExpr
354
354
  read_identifiers(expression.expression, names, reads_info)
355
+ when AST::PrefixCast
356
+ read_identifiers(expression.expression, names, reads_info)
357
+ when AST::DetachExpr
358
+ expression.body.each do |stmt|
359
+ read_identifiers(stmt.expression, names, reads_info) if stmt.is_a?(AST::ExpressionStmt)
360
+ end
355
361
  when AST::FormatString
356
362
  expression.parts.each do |part|
357
363
  read_identifiers(part.expression, names, reads_info) if part.is_a?(AST::FormatExprPart)
@@ -433,6 +439,12 @@ module MilkTea
433
439
  end
434
440
  when AST::UnsafeExpr
435
441
  write_targets_from_expression(expression.expression, line:, writes:, writes_info:)
442
+ when AST::PrefixCast
443
+ write_targets_from_expression(expression.expression, line:, writes:, writes_info:)
444
+ when AST::DetachExpr
445
+ expression.body.each do |stmt|
446
+ write_targets_from_expression(stmt.expression, line:, writes:, writes_info:) if stmt.is_a?(AST::ExpressionStmt)
447
+ end
436
448
  when AST::FormatString
437
449
  expression.parts.each do |part|
438
450
  write_targets_from_expression(part.expression, line:, writes:, writes_info:) if part.is_a?(AST::FormatExprPart)
@@ -10,11 +10,11 @@ module MilkTea
10
10
  POINTER_SIZE = Fiddle::SIZEOF_VOIDP
11
11
 
12
12
  def self.size_of(type)
13
- size_and_alignment(type, {})&.first
13
+ size_and_alignment(type, nil)&.first
14
14
  end
15
15
 
16
16
  def self.alignment_of(type)
17
- size_and_alignment(type, {})&.last
17
+ size_and_alignment(type, nil)&.last
18
18
  end
19
19
 
20
20
  def self.offset_of(type, field_name)
@@ -38,6 +38,14 @@ module MilkTea
38
38
  end
39
39
  end
40
40
 
41
+ # Computes [size, alignment] for a type.
42
+ #
43
+ # `stack` carries the aggregates on the current root-to-leaf path,
44
+ # pass-down only (never popped). A field whose type reaches any aggregate
45
+ # on that path participates in a storage cycle, and the C backend embeds
46
+ # exactly those fields as pointers (see
47
+ # CBackend#build_cyclic_aggregate_pairs and the cyclic field emission in
48
+ # type_declaration.rb), so layout sizes back-edge fields as raw pointers.
41
49
  def self.size_and_alignment(type, stack)
42
50
  case type
43
51
  when Types::Primitive
@@ -48,24 +56,24 @@ module MilkTea
48
56
  if type.is_a?(Types::Function) || layout_pointer_like_nullable_base?(type.base)
49
57
  [POINTER_SIZE, POINTER_SIZE]
50
58
  else
51
- with_stack(type, stack) do
52
- layout = struct_layout(nullable_opt_layout_fields(type.base), packed: false, alignment: nil, stack:)
59
+ with_stack(type, stack) do |next_stack|
60
+ layout = struct_layout(nullable_opt_layout_fields(type.base), packed: false, alignment: nil, stack: next_stack)
53
61
  [layout[:size], layout[:alignment]]
54
62
  end
55
63
  end
56
64
  when Types::StringView, Types::Span, Types::Task, Types::Struct, Types::StructInstance
57
- with_stack(type, stack) do
58
- layout = struct_layout(ordered_fields(type), packed: packed_layout?(type), alignment: explicit_alignment(type), stack:)
65
+ with_stack(type, stack) do |next_stack|
66
+ layout = struct_layout(ordered_fields(type), packed: packed_layout?(type), alignment: explicit_alignment(type), stack: next_stack)
59
67
  [layout[:size], layout[:alignment]]
60
68
  end
61
69
  when Types::Union
62
- with_stack(type, stack) do
63
- layout = union_layout(ordered_fields(type), packed: packed_layout?(type), alignment: explicit_alignment(type), stack:)
70
+ with_stack(type, stack) do |next_stack|
71
+ layout = union_layout(ordered_fields(type), packed: packed_layout?(type), alignment: explicit_alignment(type), stack: next_stack)
64
72
  [layout[:size], layout[:alignment]]
65
73
  end
66
74
  when Types::Variant, Types::VariantInstance
67
- with_stack(type, stack) do
68
- layout = variant_layout(type, stack:)
75
+ with_stack(type, stack) do |next_stack|
76
+ layout = variant_layout(type, stack: next_stack)
69
77
  [layout[:size], layout[:alignment]]
70
78
  end
71
79
  when Types::GenericInstance
@@ -128,8 +136,8 @@ module MilkTea
128
136
  when "str_buffer"
129
137
  return unless str_buffer_type?(type)
130
138
 
131
- with_stack(type, stack) do
132
- layout = struct_layout(str_buffer_fields(type), packed: false, alignment: nil, stack:)
139
+ with_stack(type, stack) do |next_stack|
140
+ layout = struct_layout(str_buffer_fields(type), packed: false, alignment: nil, stack: next_stack)
133
141
  [layout[:size], layout[:alignment]]
134
142
  end
135
143
  end
@@ -151,7 +159,7 @@ module MilkTea
151
159
 
152
160
  def self.struct_layout(fields, packed:, alignment:, stack:)
153
161
  field_infos = fields.map do |field_name, field_type|
154
- field_layout = size_and_alignment(field_type, stack)
162
+ field_layout = field_size_and_alignment(field_type, stack)
155
163
  return nil unless field_layout
156
164
 
157
165
  [field_name, field_layout.first, field_layout.last]
@@ -162,7 +170,7 @@ module MilkTea
162
170
 
163
171
  def self.union_layout(fields, packed:, alignment:, stack:)
164
172
  field_infos = fields.map do |_field_name, field_type|
165
- field_layout = size_and_alignment(field_type, stack)
173
+ field_layout = field_size_and_alignment(field_type, stack)
166
174
  return nil unless field_layout
167
175
 
168
176
  field_layout
@@ -171,6 +179,30 @@ module MilkTea
171
179
  union_layout_from_layouts(field_infos.map { |size, field_alignment| { size:, alignment: field_alignment } }, packed:, alignment:)
172
180
  end
173
181
 
182
+ # A field whose type reaches any aggregate on the current path is a
183
+ # storage-cycle back edge. The C backend emits such fields as pointers
184
+ # (element pointer for arrays), so they occupy pointer-size storage here.
185
+ def self.field_size_and_alignment(field_type, stack)
186
+ return [POINTER_SIZE, POINTER_SIZE] if cyclic_back_edge?(field_type, stack)
187
+
188
+ size_and_alignment(field_type, stack)
189
+ end
190
+
191
+ def self.cyclic_back_edge?(field_type, stack)
192
+ return false if stack.nil? || stack.empty?
193
+
194
+ aggregate_reaches_in_progress?(field_type, stack, {})
195
+ end
196
+
197
+ def self.aggregate_reaches_in_progress?(type, stack, visited)
198
+ return false if visited.key?(type.object_id)
199
+
200
+ visited[type.object_id] = true
201
+ return true if stack.key?(type)
202
+
203
+ reachability_children(type).any? { |child| aggregate_reaches_in_progress?(child, stack, visited) }
204
+ end
205
+
174
206
  def self.struct_layout_from_infos(field_infos, packed:, alignment:)
175
207
  offsets = {}
176
208
  offset = 0
@@ -253,13 +285,52 @@ module MilkTea
253
285
  remainder.zero? ? value : value + alignment - remainder
254
286
  end
255
287
 
288
+ # `stack` is the set of aggregates on the current path, pass-down only.
289
+ # A nil or empty stack means the caller is not inside an aggregate yet.
290
+ # Yields the extended stack; returns nil when `type` is already in progress
291
+ # (callers treat that as unreachable while mirroring backend semantics).
256
292
  def self.with_stack(type, stack)
257
- return if stack[type]
293
+ next_stack = (stack || {}).dup
294
+ return nil if next_stack.key?(type)
295
+
296
+ next_stack[type] = true
297
+ yield(next_stack)
298
+ end
299
+
300
+ # Structural expansion used for back-edge detection. Mirrors the
301
+ # CBackend's `aggregate_type_dependencies` so pointer-broken cyclic
302
+ # fields agree with the emitted C struct layout.
303
+ def self.reachability_children(type)
304
+ case type
305
+ when Types::Nullable
306
+ [type.base]
307
+ when Types::GenericInstance
308
+ if pointer_type?(type)
309
+ []
310
+ elsif array_type?(type)
311
+ [array_element_type(type)]
312
+ else
313
+ []
314
+ end
315
+ when Types::Span
316
+ [type.element_type]
317
+ when Types::Struct, Types::StructInstance, Types::Union
318
+ type.fields.values
319
+ when Types::Variant, Types::VariantInstance
320
+ type.arm_names.flat_map { |name| type.arm(name).values }
321
+ when Types::VariantArmPayload
322
+ type.fields.values
323
+ else
324
+ []
325
+ end
326
+ end
327
+
328
+ def self.pointer_type?(type)
329
+ type.is_a?(Types::GenericInstance) && %w[ptr const_ptr own ref].include?(type.name)
330
+ end
258
331
 
259
- stack[type] = true
260
- yield
261
- ensure
262
- stack.delete(type)
332
+ def self.array_element_type(type)
333
+ type.arguments.first
263
334
  end
264
335
  end
265
336
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandStd
6
+ def std_command
7
+ subcommand = @argv.shift
8
+
9
+ case subcommand
10
+ when "list"
11
+ std_list_command
12
+ when "show"
13
+ std_show_command
14
+ when nil
15
+ @err.puts("missing std subcommand (expected list or show)")
16
+ print_command_help("std", @err)
17
+ 1
18
+ else
19
+ @err.puts("unknown std subcommand #{subcommand}")
20
+ print_command_help("std", @err)
21
+ 1
22
+ end
23
+ end
24
+
25
+ def std_list_command
26
+ as_json = false
27
+ until @argv.empty?
28
+ option = @argv.shift
29
+ case option
30
+ when "--json"
31
+ as_json = true
32
+ else
33
+ @err.puts("unknown std list option #{option}")
34
+ return 1
35
+ end
36
+ end
37
+
38
+ entries = StdCatalog.entries
39
+ if as_json
40
+ @out.puts(JSON.generate(entries.map(&:to_h)))
41
+ return 0
42
+ end
43
+
44
+ print_std_list(entries)
45
+ 0
46
+ end
47
+
48
+ def print_std_list(entries)
49
+ name_width = entries.map { |entry| entry.name.length }.max
50
+ description_width = entries.map { |entry| entry.description&.length.to_i }.max
51
+
52
+ StdCatalog::CATEGORY_ORDER.each do |category|
53
+ category_entries = entries.select { |entry| entry.category == category }
54
+ next if category_entries.empty?
55
+
56
+ @out.puts("#{category}:")
57
+ category_entries.each do |entry|
58
+ description = entry.description || "(no description)"
59
+ @out.puts(" #{entry.name.ljust(name_width)} #{description.ljust(description_width)} #{entry.path}")
60
+ end
61
+ @out.puts
62
+ end
63
+ end
64
+
65
+ def std_show_command
66
+ name = @argv.shift
67
+ unless name
68
+ @err.puts("missing module name")
69
+ print_command_help("std", @err)
70
+ return 1
71
+ end
72
+
73
+ path = StdCatalog.resolve(name)
74
+ unless path
75
+ @err.puts("std module not found: #{name}")
76
+ return 1
77
+ end
78
+
79
+ @out.write(File.read(path))
80
+ 0
81
+ end
82
+ end
83
+ end
84
+ end
@@ -33,6 +33,7 @@ module MilkTea
33
33
  ["bindgen", "Generate a binding module from a C header"],
34
34
  ["cache", "Inspect and manage the build cache"],
35
35
  ["docs", "Serve the local documentation site"],
36
+ ["std", "List standard library modules and print their source"],
36
37
  ["snapshot", "Render a highlighted HTML snapshot"],
37
38
  ["lsp", "Start the Language Server Protocol server"],
38
39
  ["dap", "Start the Debug Adapter Protocol server"],
@@ -127,6 +128,8 @@ module MilkTea
127
128
  cache_command
128
129
  when "docs"
129
130
  docs_command
131
+ when "std"
132
+ std_command
130
133
  when "snapshot"
131
134
  snapshot_command
132
135
  when "lsp"
@@ -164,6 +167,7 @@ module MilkTea
164
167
  require_relative "cli/commands/bindgen"
165
168
  require_relative "cli/commands/cache"
166
169
  require_relative "cli/commands/docs"
170
+ require_relative "cli/commands/std"
167
171
  require_relative "cli/commands/snapshot"
168
172
  require_relative "cli/commands/lsp"
169
173
  require_relative "cli/commands/dap"
@@ -187,6 +191,7 @@ module MilkTea
187
191
  include CommandBindgen
188
192
  include CommandCache
189
193
  include CommandDocs
194
+ include CommandStd
190
195
  include CommandSnapshot
191
196
  include CommandLsp
192
197
  include CommandDap
@@ -796,6 +801,24 @@ module MilkTea
796
801
  --open, -o Open the docs in your default browser.
797
802
  --port, -p PORT Listen on a specific port (default: random).
798
803
  HELP
804
+ "std" => <<~HELP,
805
+ Usage: mtc std list [--json]
806
+ mtc std show MODULE
807
+
808
+ Inspect the Milk Tea standard library shipped in std/.
809
+
810
+ list Print every hand-written standard library module with a short
811
+ description and its path on disk, grouped by category.
812
+ Generated binding modules (std/c/* and imported-binding
813
+ wrappers such as raylib or zstd) are not listed.
814
+ show Print the source of a module. Accepts dotted or slashed names
815
+ (mem.arena, mem/arena) and resolves platform variants such as
816
+ fs to the active platform file. Generated modules stay
817
+ viewable here (for example c.cjson or raylib).
818
+
819
+ Options:
820
+ --json Emit list output as JSON.
821
+ HELP
799
822
  "snapshot" => <<~HELP,
800
823
  Usage: mtc snapshot INPUT.mt [OPTIONS]
801
824
 
@@ -891,6 +914,8 @@ module MilkTea
891
914
  io.puts(" mtc bindgen MODULE HEADER [-o OUTPUT] [--nullable-report PATH] [--link LIB] [--include HEADER] [--clang PATH] [--clang-arg ARG]")
892
915
  io.puts(" mtc cache purge|status")
893
916
  io.puts(" mtc docs [--open] [--port PORT]")
917
+ io.puts(" mtc std list [--json]")
918
+ io.puts(" mtc std show MODULE")
894
919
  io.puts(" mtc snapshot INPUT.mt [--theme PATH] [-o OUTPUT]")
895
920
  io.puts(" mtc lsp [--log-level LEVEL] [--stdio]")
896
921
  io.puts(" mtc dap [--backend KIND] [--adapter-path PATH]")
@@ -17,6 +17,7 @@ module MilkTea
17
17
  when "redundant-else" then redundant_else_edits(lines, warning)
18
18
  when "redundant-return" then redundant_return_edits(lines, warning)
19
19
  when "redundant-type-annotation" then redundant_type_annotation_edits(lines, warning)
20
+ when "redundant-unsafe" then redundant_unsafe_edits(lines, warning)
20
21
  when "prefer-inline-methods" then prefer_inline_methods_edits(lines, warning)
21
22
  when "unused-import" then unused_import_edits(lines, warning)
22
23
  when "trailing-list-comma" then trailing_list_comma_edits(lines, warning)
@@ -302,6 +303,105 @@ module MilkTea
302
303
  edits
303
304
  end
304
305
 
306
+ # ── redundant-unsafe ───────────────────────────────────────────────────
307
+ # Removes an `unsafe:` wrapper that contains no unsafe operations.
308
+ #
309
+ # Supported shapes:
310
+ # <indent>unsafe:\n<deeper body> (block statement; body is dedented)
311
+ # ... unsafe: expr ... (inline expression on a single line)
312
+ def self.redundant_unsafe_edits(lines, warning)
313
+ return [] unless warning.line
314
+
315
+ line_idx = warning.line - 1
316
+ line = lines[line_idx]
317
+ return [] unless line
318
+
319
+ unsafe_idx = line.index("unsafe:")
320
+ return [] unless unsafe_idx
321
+
322
+ keyword_end = unsafe_idx + "unsafe:".length
323
+ rest = line[keyword_end..]
324
+
325
+ if rest && !rest.strip.empty?
326
+ # Inline expression form: drop the keyword and the whitespace after
327
+ # it, but only when the remainder is balanced on this line.
328
+ remainder = rest.sub(/\A\s+/, "")
329
+ return [] if remainder.empty? || !balanced_line?(remainder)
330
+
331
+ return [FixEdit.new(
332
+ start_line: line_idx,
333
+ start_char: unsafe_idx,
334
+ end_line: line_idx,
335
+ end_char: keyword_end + (rest.length - remainder.length),
336
+ new_text: "",
337
+ )]
338
+ end
339
+
340
+ # Block statement form: `unsafe:` alone, body indented deeper.
341
+ indent = line[0...unsafe_idx]
342
+ return [] unless indent.match?(/\A *\z/)
343
+
344
+ body_start = line_idx + 1
345
+ body_indent = nil
346
+ last_body = nil
347
+ (body_start...lines.length).each do |i|
348
+ candidate = lines[i]
349
+ if candidate.nil? || candidate.strip.empty?
350
+ last_body = i
351
+ next
352
+ end
353
+
354
+ break unless candidate.start_with?(indent + " ")
355
+
356
+ body_indent ||= candidate[/\A */].length - indent.length
357
+ last_body = i
358
+ end
359
+
360
+ return [] if last_body.nil? || last_body < body_start
361
+ return [] if body_indent.nil? || body_indent <= 0
362
+ return [] unless (body_start..last_body).all? do |i|
363
+ l = lines[i]
364
+ l.nil? || l.strip.empty? || l.start_with?(indent + " " * body_indent)
365
+ end
366
+
367
+ replacement = (body_start..last_body).map do |i|
368
+ l = lines[i]
369
+ l.nil? || l.strip.empty? ? "" : l[body_indent..].to_s.chomp
370
+ end
371
+
372
+ if last_body + 1 <= lines.length - 1
373
+ # Consume through the first following line; apply_fix_edits re-emits
374
+ # it as the suffix of a multi-line replacement.
375
+ end_line = last_body + 1
376
+ end_char = 0
377
+ else
378
+ end_line = last_body
379
+ end_char = lines[last_body].to_s.length
380
+ end
381
+
382
+ [FixEdit.new(
383
+ start_line: line_idx,
384
+ start_char: 0,
385
+ end_line:,
386
+ end_char:,
387
+ new_text: replacement.join("\n") + "\n",
388
+ )]
389
+ end
390
+
391
+ def self.balanced_line?(text)
392
+ depth = 0
393
+ text.each_char do |char|
394
+ case char
395
+ when "(", "["
396
+ depth += 1
397
+ when ")", "]"
398
+ depth -= 1
399
+ return false if depth.negative?
400
+ end
401
+ end
402
+ depth.zero?
403
+ end
404
+
305
405
  def self.match_paren(line, open_pos)
306
406
  depth = 0
307
407
  (open_pos...line.length).each do |i|
@@ -258,10 +258,28 @@ module MilkTea
258
258
  case expr
259
259
  when AST::Call
260
260
  expr.arguments.any? do |arg|
261
- next unless arg.is_a?(AST::Argument)
261
+ next false unless arg.is_a?(AST::Argument)
262
262
 
263
- arg.value.is_a?(AST::Identifier) && arg.value.name == name ||
264
- own_struct_field_transfer?(arg.value, name)
263
+ own_transfer_identifier?(arg.value, name)
264
+ end
265
+ when AST::PrefixCast
266
+ own_transfer_identifier?(expr.expression, name)
267
+ else
268
+ false
269
+ end
270
+ end
271
+
272
+ def own_transfer_identifier?(value, name)
273
+ case value
274
+ when AST::Identifier
275
+ value.name == name
276
+ when AST::PrefixCast
277
+ own_transfer_identifier?(value.expression, name)
278
+ when AST::Call
279
+ value.arguments.any? do |arg|
280
+ next false unless arg.is_a?(AST::Argument)
281
+
282
+ own_transfer_identifier?(arg.value, name)
265
283
  end
266
284
  else
267
285
  false
@@ -426,15 +444,29 @@ module MilkTea
426
444
  return false unless expr.is_a?(AST::Call)
427
445
 
428
446
  expr.arguments.any? do |arg|
429
- next false unless arg.is_a?(AST::Argument) && arg.name
430
-
431
- if arg.value.is_a?(AST::Identifier) && arg.value.name == name
432
- true
433
- elsif arg.value.is_a?(AST::Call)
434
- _expr_contains_transfer?(arg.value, name)
435
- else
436
- false
447
+ named = arg.is_a?(AST::Argument) && !arg.name.nil?
448
+ argument_transfers?(arg.value, name, named)
449
+ end
450
+ end
451
+
452
+ # An identifier passed as a named argument transfers ownership (struct
453
+ # field initialization), even when wrapped in a pointer cast
454
+ # (e.g. handle_set_data(h, ptr[void]<-state)). Nested calls are walked
455
+ # so `queue.push(Foo(field = x))` recognizes the inner transfer.
456
+ def argument_transfers?(value, name, named)
457
+ case value
458
+ when AST::Identifier
459
+ named && value.name == name
460
+ when AST::PrefixCast
461
+ inner = value.expression
462
+ inner.is_a?(AST::Identifier) && inner.name == name
463
+ when AST::Call
464
+ value.arguments.any? do |arg|
465
+ nested_named = arg.is_a?(AST::Argument) && !arg.name.nil?
466
+ argument_transfers?(arg.value, name, nested_named)
437
467
  end
468
+ else
469
+ false
438
470
  end
439
471
  end
440
472
 
@@ -481,6 +513,9 @@ module MilkTea
481
513
  return if expr.nil?
482
514
 
483
515
  result << expr.callee.receiver.name if release_call_on_binding?(expr)
516
+ if expr.is_a?(AST::Call) && expr.callee.is_a?(AST::Identifier) && expr.callee.name.start_with?("release_")
517
+ result.merge(release_helper_binding_names(expr))
518
+ end
484
519
 
485
520
  case expr
486
521
  when AST::Call
@@ -496,6 +531,26 @@ module MilkTea
496
531
  end
497
532
  end
498
533
 
534
+ # std convention: `release_*` helper functions take ownership of the
535
+ # bindings handed to them via ref_of/ptr_of, e.g.
536
+ # `release_string_values(ref_of(values))`. Treat those bindings as
537
+ # released so the leak rule understands the cleanup-helper idiom.
538
+ def release_helper_binding_names(expr)
539
+ names = Set.new
540
+ expr.arguments.each do |arg|
541
+ value = arg.respond_to?(:value) ? arg.value : nil
542
+ next unless value.is_a?(AST::Call)
543
+ next unless value.callee.is_a?(AST::Identifier)
544
+ next unless %w[ref_of ptr_of].include?(value.callee.name)
545
+
546
+ value.arguments.each do |inner|
547
+ binding_name = inner.value
548
+ names << binding_name.name if binding_name.is_a?(AST::Identifier)
549
+ end
550
+ end
551
+ names
552
+ end
553
+
499
554
  def _any_stmt?(stmts, &pred)
500
555
  return false if stmts.nil?
501
556
 
@@ -620,6 +675,9 @@ module MilkTea
620
675
  return false unless expr
621
676
 
622
677
  return true if release_call_on_binding?(expr) && expr.callee.receiver.name == name
678
+ return true if expr.is_a?(AST::Call) && expr.callee.is_a?(AST::Identifier) &&
679
+ expr.callee.name.start_with?("release_") &&
680
+ release_helper_binding_names(expr).include?(name)
623
681
 
624
682
  case expr
625
683
  when AST::Call