mt-lang 0.4.24 → 0.4.26
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 +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +8 -8
- data/README.md +39 -7
- data/docs/build-guide.md +5 -0
- data/docs/index.html +61 -29
- data/docs/language-design.md +2 -2
- data/docs/language-manual.md +15 -3
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/bindings/raw_bindings/defaults.rb +22 -0
- data/lib/milk_tea/core/c_backend/statements.rb +118 -1
- data/lib/milk_tea/core/control_flow/builder.rb +12 -0
- data/lib/milk_tea/core/lowering/utils.rb +8 -1
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +43 -2
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +7 -1
- data/lib/milk_tea/tooling/bindgen_cli.rb +56 -0
- data/lib/milk_tea/tooling/cli/commands/std.rb +84 -0
- data/lib/milk_tea/tooling/cli.rb +27 -1
- data/lib/milk_tea/tooling/linter/fix_engine.rb +100 -0
- data/lib/milk_tea/tooling/linter/release_rules.rb +69 -11
- data/lib/milk_tea/tooling/linter/visitors.rb +58 -31
- data/lib/milk_tea/tooling/linter.rb +8 -1
- data/lib/milk_tea/tooling/std_catalog.rb +277 -0
- data/lib/milk_tea/tooling.rb +1 -0
- data/std/async/libuv_runtime.mt +20 -23
- data/std/async/mailbox.mt +2 -2
- data/std/base64.mt +3 -6
- data/std/behavior_tree.mt +1 -6
- data/std/binary.mt +2 -5
- data/std/c/stb_image.mt +15 -15
- data/std/color.mt +20 -20
- data/std/cookie.mt +1 -4
- data/std/deque.mt +2 -8
- data/std/encoding.mt +1 -4
- data/std/fmt.mt +1 -4
- data/std/graph.mt +5 -1
- data/std/htn.mt +9 -1
- data/std/http/server.mt +1 -4
- data/std/http.mt +14 -17
- data/std/jobs.mt +9 -10
- data/std/json.mt +17 -28
- data/std/linked_map.mt +3 -9
- data/std/map.mt +1 -4
- data/std/net/channel.mt +6 -6
- data/std/net/lobby.mt +3 -3
- data/std/net/manager.mt +0 -1
- data/std/net/mux.mt +1 -1
- data/std/net/packet.mt +4 -1
- data/std/net/punch.mt +3 -2
- data/std/net/rpc.mt +1 -3
- data/std/net/session.mt +12 -25
- data/std/net/sync.mt +3 -3
- data/std/net/turn.mt +7 -3
- data/std/net.mt +145 -159
- data/std/noise.mt +11 -7
- data/std/ordered_map.mt +2 -8
- data/std/ordered_set.mt +3 -5
- data/std/path.mt +1 -4
- data/std/process.mt +7 -18
- data/std/random.mt +2 -2
- data/std/sdl3/runtime.mt +1 -1
- data/std/steering.mt +27 -4
- data/std/string.mt +1 -4
- data/std/tar.mt +8 -10
- data/std/terminal.mt +21 -40
- data/std/thread.mt +3 -4
- data/std/tls.mt +22 -29
- data/std/toml.mt +23 -31
- data/std/uri.mt +2 -1
- data/std/utility.mt +2 -2
- data/std/vec.mt +7 -17
- metadata +5 -3
|
@@ -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)
|
|
@@ -127,6 +127,13 @@ module MilkTea
|
|
|
127
127
|
|
|
128
128
|
def infer_value_type(handle_expression, env:)
|
|
129
129
|
handle_type = infer_expression_type(handle_expression, env:)
|
|
130
|
+
# BUG FIX: nullable pointer handles (ptr[T]?) read through as their
|
|
131
|
+
# base pointer type; std/net relies on this after flow-unguarded
|
|
132
|
+
# null checks on storage fields.
|
|
133
|
+
if handle_type.is_a?(Types::Nullable) &&
|
|
134
|
+
(ref_type?(handle_type.base) || pointer_type?(handle_type.base))
|
|
135
|
+
handle_type = handle_type.base
|
|
136
|
+
end
|
|
130
137
|
return referenced_type(handle_type) if ref_type?(handle_type)
|
|
131
138
|
return pointee_type(handle_type) if pointer_type?(handle_type)
|
|
132
139
|
|
|
@@ -371,7 +378,7 @@ module MilkTea
|
|
|
371
378
|
elements = value.map { |element| lower_compile_time_literal(element, element_type) }
|
|
372
379
|
return nil if elements.any?(&:nil?)
|
|
373
380
|
|
|
374
|
-
IR::ArrayLiteral.new(type:, elements:)
|
|
381
|
+
return IR::ArrayLiteral.new(type:, elements:)
|
|
375
382
|
when Hash
|
|
376
383
|
return nil unless type.is_a?(Types::Struct)
|
|
377
384
|
fields = value.map do |name, field_value|
|
|
@@ -721,8 +721,10 @@ module MilkTea
|
|
|
721
721
|
raise_sema_error("operator #{expression.operator} is not supported for type #{bad_type}")
|
|
722
722
|
end
|
|
723
723
|
end
|
|
724
|
-
unless common_numeric_type(left_type, right_type) || types_compatible?(left_type, right_type) || types_compatible?(right_type, left_type)
|
|
725
|
-
|
|
724
|
+
unless common_numeric_type(left_type, right_type) || types_compatible?(left_type, right_type) || types_compatible?(right_type, left_type) || refined_null_comparison?(expression, scopes)
|
|
725
|
+
message = "operator #{expression.operator} requires comparable types, got #{left_type} and #{right_type}"
|
|
726
|
+
hint = null_comparison_hint(left_type, right_type)
|
|
727
|
+
raise_sema_error(hint ? "#{message}; #{hint}" : message)
|
|
726
728
|
end
|
|
727
729
|
|
|
728
730
|
@ctx.types.fetch("bool")
|
|
@@ -731,6 +733,45 @@ module MilkTea
|
|
|
731
733
|
end
|
|
732
734
|
end
|
|
733
735
|
|
|
736
|
+
# A null comparison on a nullable binding stays legal after flow
|
|
737
|
+
# refinement has narrowed the binding to non-null: the declared storage
|
|
738
|
+
# is still nullable, and the check is a redundant-but-valid test (the
|
|
739
|
+
# linter reports it as redundant-null-check). Only bare `null` qualifies;
|
|
740
|
+
# a typed `null[...]` with a mismatched target stays a type error.
|
|
741
|
+
def refined_null_comparison?(expression, scopes)
|
|
742
|
+
if expression.left.is_a?(AST::NullLiteral) && expression.left.type.nil? && expression.right.is_a?(AST::Identifier)
|
|
743
|
+
identifier_expression = expression.right
|
|
744
|
+
elsif expression.right.is_a?(AST::NullLiteral) && expression.right.type.nil? && expression.left.is_a?(AST::Identifier)
|
|
745
|
+
identifier_expression = expression.left
|
|
746
|
+
else
|
|
747
|
+
return false
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
binding = lookup_value(identifier_expression.name, scopes)
|
|
751
|
+
binding&.storage_type.is_a?(Types::Nullable)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# A null comparison against a non-nullable operand is a contract mismatch,
|
|
755
|
+
# not just incompatible operand types: the message should say which fix
|
|
756
|
+
# applies (nullable declaration, raw zero comparison, or removal).
|
|
757
|
+
def null_comparison_hint(left_type, right_type)
|
|
758
|
+
other = if left_type.is_a?(Types::Null) && !right_type.is_a?(Types::Null)
|
|
759
|
+
right_type
|
|
760
|
+
elsif right_type.is_a?(Types::Null) && !left_type.is_a?(Types::Null)
|
|
761
|
+
left_type
|
|
762
|
+
end
|
|
763
|
+
return nil unless other
|
|
764
|
+
return nil if other.is_a?(Types::Nullable) || other.is_a?(Types::Null)
|
|
765
|
+
|
|
766
|
+
if ref_type?(other)
|
|
767
|
+
"#{other} is never null by design; refs cannot be compared against null"
|
|
768
|
+
elsif pointer_type?(other) || opaque_type?(other) || other == Types::Registry.primitive("cstr")
|
|
769
|
+
hint = "#{other} is declared non-null; declare it #{other}? if null is possible"
|
|
770
|
+
hint << ", or compare against zero[#{other}] for raw zero-pointer storage" if zero_supported_type?(other)
|
|
771
|
+
hint
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
|
|
734
775
|
def infer_if_expression(expression, scopes:, expected_type: nil)
|
|
735
776
|
condition_type = infer_expression(expression.condition, scopes:, expected_type: @ctx.types.fetch("bool"))
|
|
736
777
|
ensure_assignable!(condition_type, @ctx.types.fetch("bool"), "if expression condition must be bool, got #{condition_type}")
|
|
@@ -740,6 +740,12 @@ module MilkTea
|
|
|
740
740
|
end
|
|
741
741
|
|
|
742
742
|
def zero_initializable_type?(type, operation: "zero")
|
|
743
|
+
return true if zero_supported_type?(type)
|
|
744
|
+
|
|
745
|
+
raise_sema_error("#{operation} does not support type #{type}")
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def zero_supported_type?(type)
|
|
743
749
|
return true if type.is_a?(Types::Primitive) && !type.void?
|
|
744
750
|
return true if type.is_a?(Types::Nullable)
|
|
745
751
|
return true if type.is_a?(Types::EnumBase)
|
|
@@ -761,7 +767,7 @@ module MilkTea
|
|
|
761
767
|
return true if simd_type?(type)
|
|
762
768
|
return true if atomic_type?(type)
|
|
763
769
|
|
|
764
|
-
|
|
770
|
+
false
|
|
765
771
|
end
|
|
766
772
|
|
|
767
773
|
def layout_aggregate_type?(type)
|
|
@@ -101,6 +101,11 @@ module MilkTea
|
|
|
101
101
|
return missing_option_value(option) unless value
|
|
102
102
|
|
|
103
103
|
options[:nullable_report_path] = value
|
|
104
|
+
when "--nullable-policy"
|
|
105
|
+
value = @argv.shift
|
|
106
|
+
return missing_option_value(option) unless value
|
|
107
|
+
|
|
108
|
+
options[:nullable_policy_path] = value
|
|
104
109
|
else
|
|
105
110
|
@err.puts("unknown bindgen option #{option}")
|
|
106
111
|
print_help
|
|
@@ -109,9 +114,60 @@ module MilkTea
|
|
|
109
114
|
end
|
|
110
115
|
|
|
111
116
|
options[:include_directives] = nil if options[:include_directives].empty?
|
|
117
|
+
if options[:nullable_policy_path]
|
|
118
|
+
policy = load_nullable_policy(options.delete(:nullable_policy_path))
|
|
119
|
+
return nil unless policy
|
|
120
|
+
|
|
121
|
+
options[:function_return_type_overrides] = policy[:return_types]
|
|
122
|
+
options[:function_param_type_overrides] = policy[:parameters]
|
|
123
|
+
end
|
|
112
124
|
options
|
|
113
125
|
end
|
|
114
126
|
|
|
127
|
+
# A nullable policy lists the symbols a header documents as returning NULL
|
|
128
|
+
# (or taking NULL-able out-pointers) so generated raw bindings expose them
|
|
129
|
+
# as nullable `T?` without hand-annotating the C header. Entries feed the
|
|
130
|
+
# same function_return_type_overrides / function_param_type_overrides
|
|
131
|
+
# machinery as the raw binding registry and appear in the nullable report.
|
|
132
|
+
def load_nullable_policy(path)
|
|
133
|
+
require "json"
|
|
134
|
+
policy_path = File.expand_path(path)
|
|
135
|
+
unless File.file?(policy_path)
|
|
136
|
+
@err.puts("nullable policy file not found: #{path}")
|
|
137
|
+
return nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
document = begin
|
|
141
|
+
JSON.parse(File.read(policy_path))
|
|
142
|
+
rescue JSON::ParserError => e
|
|
143
|
+
@err.puts("nullable policy is not valid JSON: #{e.message}")
|
|
144
|
+
return nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
unless document.is_a?(Hash)
|
|
148
|
+
@err.puts("nullable policy must be a JSON object")
|
|
149
|
+
return nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
unknown_keys = document.keys - ["return_types", "parameters"]
|
|
153
|
+
unless unknown_keys.empty?
|
|
154
|
+
@err.puts("nullable policy has unknown keys: #{unknown_keys.join(', ')} (expected return_types, parameters)")
|
|
155
|
+
return nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
return_types = document["return_types"] || {}
|
|
159
|
+
parameters = document["parameters"] || {}
|
|
160
|
+
policy_shape_valid = return_types.is_a?(Hash) && return_types.keys.all?(String) && return_types.values.all?(String) &&
|
|
161
|
+
parameters.is_a?(Hash) && parameters.keys.all?(String) &&
|
|
162
|
+
parameters.values.all? { |value| value.is_a?(Hash) && value.keys.all?(String) && value.values.all?(String) }
|
|
163
|
+
unless policy_shape_valid
|
|
164
|
+
@err.puts('nullable policy must map {"return_types": {symbol: "type?"}, "parameters": {symbol: {param: "type?"}}}')
|
|
165
|
+
return nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
{ return_types:, parameters: }
|
|
169
|
+
end
|
|
170
|
+
|
|
115
171
|
def missing_option_value(option)
|
|
116
172
|
@err.puts("missing value for #{option}")
|
|
117
173
|
print_help
|
|
@@ -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
|
data/lib/milk_tea/tooling/cli.rb
CHANGED
|
@@ -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
|
|
@@ -771,6 +776,7 @@ module MilkTea
|
|
|
771
776
|
|
|
772
777
|
Options:
|
|
773
778
|
-o, --output OUTPUT Write the generated module to this file.
|
|
779
|
+
--nullable-policy PATH Apply a JSON nullable policy of symbols documented as returning NULL.
|
|
774
780
|
--nullable-report PATH Write the remaining manual nullable policy report to this file.
|
|
775
781
|
--link LIB Link against this library (repeatable).
|
|
776
782
|
--include HEADER Extra #include directive (repeatable).
|
|
@@ -796,6 +802,24 @@ module MilkTea
|
|
|
796
802
|
--open, -o Open the docs in your default browser.
|
|
797
803
|
--port, -p PORT Listen on a specific port (default: random).
|
|
798
804
|
HELP
|
|
805
|
+
"std" => <<~HELP,
|
|
806
|
+
Usage: mtc std list [--json]
|
|
807
|
+
mtc std show MODULE
|
|
808
|
+
|
|
809
|
+
Inspect the Milk Tea standard library shipped in std/.
|
|
810
|
+
|
|
811
|
+
list Print every hand-written standard library module with a short
|
|
812
|
+
description and its path on disk, grouped by category.
|
|
813
|
+
Generated binding modules (std/c/* and imported-binding
|
|
814
|
+
wrappers such as raylib or zstd) are not listed.
|
|
815
|
+
show Print the source of a module. Accepts dotted or slashed names
|
|
816
|
+
(mem.arena, mem/arena) and resolves platform variants such as
|
|
817
|
+
fs to the active platform file. Generated modules stay
|
|
818
|
+
viewable here (for example c.cjson or raylib).
|
|
819
|
+
|
|
820
|
+
Options:
|
|
821
|
+
--json Emit list output as JSON.
|
|
822
|
+
HELP
|
|
799
823
|
"snapshot" => <<~HELP,
|
|
800
824
|
Usage: mtc snapshot INPUT.mt [OPTIONS]
|
|
801
825
|
|
|
@@ -888,9 +912,11 @@ module MilkTea
|
|
|
888
912
|
io.puts(" mtc deps lock [PATH_OR_PACKAGE] [--check]")
|
|
889
913
|
io.puts(" mtc deps publish [PATH_OR_PACKAGE] [--upstream]")
|
|
890
914
|
io.puts(" mtc deps fetch [PATH_OR_PACKAGE]")
|
|
891
|
-
io.puts(" mtc bindgen MODULE HEADER [-o OUTPUT] [--nullable-report PATH] [--link LIB] [--include HEADER] [--clang PATH] [--clang-arg ARG]")
|
|
915
|
+
io.puts(" mtc bindgen MODULE HEADER [-o OUTPUT] [--nullable-policy PATH] [--nullable-report PATH] [--link LIB] [--include HEADER] [--clang PATH] [--clang-arg ARG]")
|
|
892
916
|
io.puts(" mtc cache purge|status")
|
|
893
917
|
io.puts(" mtc docs [--open] [--port PORT]")
|
|
918
|
+
io.puts(" mtc std list [--json]")
|
|
919
|
+
io.puts(" mtc std show MODULE")
|
|
894
920
|
io.puts(" mtc snapshot INPUT.mt [--theme PATH] [-o OUTPUT]")
|
|
895
921
|
io.puts(" mtc lsp [--log-level LEVEL] [--stdio]")
|
|
896
922
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|