mt-lang 0.3.16 → 0.3.20

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/docs/language-design.md +3 -4
  3. data/docs/language-manual.md +3 -3
  4. data/lib/milk_tea/base.rb +1 -1
  5. data/lib/milk_tea/core/{binding_types.rb → bindings.rb} +1 -1
  6. data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
  7. data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
  8. data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
  9. data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
  10. data/lib/milk_tea/core/c_backend.rb +63 -54
  11. data/lib/milk_tea/core/compile_time.rb +279 -74
  12. data/lib/milk_tea/core/control_flow/constant_propagation.rb +1 -1
  13. data/lib/milk_tea/core/{compatibility_helpers.rb → intrinsics.rb} +9 -2
  14. data/lib/milk_tea/core/lexer.rb +55 -48
  15. data/lib/milk_tea/core/lowering/block.rb +9 -0
  16. data/lib/milk_tea/core/lowering/calls.rb +2 -0
  17. data/lib/milk_tea/core/lowering/declarations.rb +1 -1
  18. data/lib/milk_tea/core/lowering/functions.rb +11 -8
  19. data/lib/milk_tea/core/lowering/resolve.rb +10 -3
  20. data/lib/milk_tea/core/lowering/scans.rb +13 -18
  21. data/lib/milk_tea/core/lowering.rb +18 -18
  22. data/lib/milk_tea/core/module_binder.rb +9 -10
  23. data/lib/milk_tea/core/module_loader.rb +64 -57
  24. data/lib/milk_tea/core/module_path_resolver.rb +1 -4
  25. data/lib/milk_tea/core/parser/declarations.rb +43 -19
  26. data/lib/milk_tea/core/parser/expressions.rb +4 -7
  27. data/lib/milk_tea/core/parser/statements.rb +5 -5
  28. data/lib/milk_tea/core/parser.rb +43 -17
  29. data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
  30. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
  31. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +117 -85
  32. data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
  33. data/lib/milk_tea/core/semantic_analyzer.rb +57 -38
  34. data/lib/milk_tea/core.rb +6 -2
  35. data/lib/milk_tea/dap/lldb_dap_backend.rb +153 -0
  36. data/lib/milk_tea/dap/server/lldb_backend.rb +1 -1
  37. data/lib/milk_tea/dap.rb +1 -1
  38. data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
  39. data/lib/milk_tea/lsp.rb +1 -7
  40. data/lib/milk_tea/{bindings/cli.rb → tooling/bindgen_cli.rb} +1 -1
  41. data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
  42. data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
  43. data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
  44. data/lib/milk_tea/tooling/cli/commands/check.rb +116 -0
  45. data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
  46. data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
  47. data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
  48. data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
  49. data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
  50. data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
  51. data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
  52. data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
  53. data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
  54. data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
  55. data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
  56. data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
  57. data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
  58. data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
  59. data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
  60. data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
  61. data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
  62. data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
  63. data/lib/milk_tea/tooling/cli.rb +101 -1893
  64. data/lib/milk_tea/tooling.rb +1 -1
  65. metadata +29 -9
  66. data/lib/milk_tea/core/compile_time/const_eval.rb +0 -182
  67. data/lib/milk_tea/core/module_loader/errors.rb +0 -23
  68. data/lib/milk_tea/dap/backends/lldb_dap.rb +0 -156
  69. /data/lib/milk_tea/core/lowering/{async.rb → async/frame_builder.rb} +0 -0
@@ -94,6 +94,7 @@ module MilkTea
94
94
  end
95
95
  ResolvedAttributeApplication = Data.define(:binding, :argument_values)
96
96
  AttributePresenceKey = Data.define(:target, :attribute_module_name, :attribute_name)
97
+ CallableResolution = Data.define(:kind, :value, :receiver)
97
98
  TypeParamConstraintBinding = Data.define(:interfaces) do
98
99
  def initialize(interfaces: []) = super
99
100
  end
@@ -129,7 +130,7 @@ module MilkTea
129
130
  end
130
131
 
131
132
  class Checker
132
- include CompatibilityHelpers
133
+ include Intrinsics
133
134
 
134
135
  attr_reader :ctx
135
136
 
@@ -185,30 +186,47 @@ module MilkTea
185
186
  end
186
187
 
187
188
  def check
188
- install_builtin_types
189
- install_builtin_attributes
190
- install_imports
191
- install_prelude_types
192
- declare_named_types
193
- resolve_generic_type_param_constraints
194
- resolve_type_aliases
195
- declare_attributes
196
- resolve_aggregate_fields
197
- resolve_enum_members
198
- resolve_variant_arms
199
- collect_emit_declarations
200
- declare_top_level_values
201
- check_attribute_applications
202
- declare_functions
203
- check_interface_conformances
204
- check_top_level_values
205
- finalize_top_level_const_values
206
- check_top_level_static_asserts
207
- check_functions
189
+ @completed_phases = Set.new
190
+
191
+ run_phase(:install_builtin_types)
192
+ run_phase(:install_builtin_attributes)
193
+ run_phase(:install_imports)
194
+ run_phase(:install_prelude_types, requires: [:install_imports])
195
+ run_phase(:declare_named_types, requires: [:install_builtin_types, :install_imports, :install_prelude_types])
196
+ run_phase(:resolve_generic_type_param_constraints, requires: [:declare_named_types])
197
+ run_phase(:resolve_type_aliases, requires: [:declare_named_types])
198
+ run_phase(:declare_attributes)
199
+ run_phase(:resolve_aggregate_fields, requires: [:resolve_type_aliases, :declare_named_types])
200
+ run_phase(:resolve_enum_members, requires: [:declare_named_types])
201
+ run_phase(:resolve_variant_arms, requires: [:declare_named_types])
202
+ run_phase(:collect_emit_declarations)
203
+ run_phase(:declare_top_level_values, requires: [:resolve_aggregate_fields, :resolve_type_aliases])
204
+ run_phase(:check_attribute_applications, requires: [:declare_attributes])
205
+ run_phase(:declare_functions, requires: [:resolve_aggregate_fields, :resolve_enum_members, :resolve_variant_arms])
206
+ run_phase(:check_interface_conformances, requires: [:declare_functions, :resolve_aggregate_fields])
207
+ run_phase(:check_top_level_values, requires: [:declare_top_level_values])
208
+ run_phase(:finalize_top_level_const_values, requires: [:check_top_level_values])
209
+ run_phase(:check_top_level_static_asserts, requires: [:finalize_top_level_const_values])
210
+ run_phase(:check_functions, requires: [:declare_functions, :resolve_aggregate_fields, :check_interface_conformances])
208
211
 
209
212
  build_analysis
210
213
  end
211
214
 
215
+ def run_phase(name, requires: [])
216
+ requires.each do |required|
217
+ unless @completed_phases&.include?(required)
218
+ raise "BUG: phase #{required} must run before #{name} — check phase ordering"
219
+ end
220
+ end
221
+ send(name)
222
+ ensure
223
+ @completed_phases << name if @completed_phases
224
+ end
225
+
226
+ def run_collecting_phase(name, requires: [])
227
+ catch_structural { run_phase(name, requires:) }
228
+ end
229
+
212
230
  def collect_emit_declarations
213
231
  collect_emit_from_declarations(expanded_declarations)
214
232
  @ctx.ast.declarations.grep(AST::ConstDecl).each { |decl| @ctx.const_declarations[decl.name] ||= decl }
@@ -274,23 +292,24 @@ module MilkTea
274
292
  def check_collecting_errors
275
293
  @collecting_errors = true
276
294
  @structural_errors = []
277
-
278
- catch_structural { install_builtin_types }
279
- catch_structural { install_builtin_attributes }
280
- catch_structural { install_imports }
281
- catch_structural { install_prelude_types }
282
- catch_structural { declare_named_types }
283
- catch_structural { resolve_generic_type_param_constraints }
284
- catch_structural { resolve_type_aliases }
285
- catch_structural { declare_attributes }
286
- catch_structural { resolve_aggregate_fields }
287
- catch_structural { resolve_enum_members }
288
- catch_structural { resolve_variant_arms }
289
- catch_structural { collect_emit_declarations }
290
- catch_structural { declare_top_level_values }
291
- catch_structural { check_attribute_applications }
292
- catch_structural { declare_functions }
293
- catch_structural { check_interface_conformances }
295
+ @completed_phases = Set.new
296
+
297
+ run_collecting_phase(:install_builtin_types)
298
+ run_collecting_phase(:install_builtin_attributes)
299
+ run_collecting_phase(:install_imports)
300
+ run_collecting_phase(:install_prelude_types, requires: [:install_imports])
301
+ run_collecting_phase(:declare_named_types, requires: [:install_builtin_types, :install_imports, :install_prelude_types])
302
+ run_collecting_phase(:resolve_generic_type_param_constraints, requires: [:declare_named_types])
303
+ run_collecting_phase(:resolve_type_aliases, requires: [:declare_named_types])
304
+ run_collecting_phase(:declare_attributes)
305
+ run_collecting_phase(:resolve_aggregate_fields, requires: [:resolve_type_aliases, :declare_named_types])
306
+ run_collecting_phase(:resolve_enum_members, requires: [:declare_named_types])
307
+ run_collecting_phase(:resolve_variant_arms, requires: [:declare_named_types])
308
+ run_collecting_phase(:collect_emit_declarations)
309
+ run_collecting_phase(:declare_top_level_values, requires: [:resolve_aggregate_fields, :resolve_type_aliases])
310
+ run_collecting_phase(:check_attribute_applications, requires: [:declare_attributes])
311
+ run_collecting_phase(:declare_functions, requires: [:resolve_aggregate_fields, :resolve_enum_members, :resolve_variant_arms])
312
+ run_collecting_phase(:check_interface_conformances, requires: [:declare_functions, :resolve_aggregate_fields])
294
313
 
295
314
  errors = @structural_errors.dup
296
315
 
data/lib/milk_tea/core.rb CHANGED
@@ -10,11 +10,15 @@ require_relative "core/cst"
10
10
  require_relative "core/cst_builder"
11
11
  require_relative "core/ast"
12
12
  require_relative "core/types"
13
- require_relative "core/binding_types"
13
+ require_relative "core/bindings"
14
14
  require_relative "core/compile_time"
15
- require_relative "core/compatibility_helpers"
15
+ require_relative "core/intrinsics"
16
16
  require_relative "core/parser"
17
17
  require_relative "core/module_roots"
18
+ require_relative "core/module_path_resolver"
19
+ require_relative "core/module_binder"
20
+ require_relative "core/async_runtime_installer"
21
+ require_relative "core/prelude_installer"
18
22
  require_relative "core/module_loader"
19
23
  require_relative "core/semantic_analyzer"
20
24
  require_relative "core/ir"
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "thread"
5
+ require "timeout"
6
+
7
+ module MilkTea
8
+ module DAP
9
+ class LLDBDAPBackend
10
+ def initialize(adapter_command: ["lldb-dap"], on_event: nil, on_request: nil)
11
+ @adapter_command = adapter_command
12
+ @on_event = on_event
13
+ @on_request = on_request
14
+ @protocol = nil
15
+ @stdin = nil
16
+ @stdout = nil
17
+ @stderr = nil
18
+ @wait_thread = nil
19
+ @reader_thread = nil
20
+ @stderr_thread = nil
21
+ @write_mutex = Mutex.new
22
+ @pending_mutex = Mutex.new
23
+ @pending = {}
24
+ @next_seq = 1
25
+ end
26
+
27
+ def start!
28
+ return if running?
29
+
30
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(*@adapter_command)
31
+ @protocol = Protocol.new(input: @stdout, output: @stdin)
32
+
33
+ @reader_thread = Thread.new { read_loop }
34
+ @stderr_thread = Thread.new { drain_stderr }
35
+ end
36
+
37
+ def running?
38
+ !@wait_thread.nil? && @wait_thread.alive?
39
+ end
40
+
41
+ def request(command, arguments = {}, timeout: 5)
42
+ start! unless running?
43
+
44
+ seq = next_seq
45
+ queue = Queue.new
46
+ @pending_mutex.synchronize do
47
+ @pending[seq] = queue
48
+ end
49
+
50
+ @write_mutex.synchronize do
51
+ @protocol.write_message({
52
+ seq: seq,
53
+ type: "request",
54
+ command: command,
55
+ arguments: arguments
56
+ })
57
+ end
58
+
59
+ Timeout.timeout(timeout) { queue.pop }
60
+ rescue Timeout::Error
61
+ {
62
+ "type" => "response",
63
+ "request_seq" => seq,
64
+ "success" => false,
65
+ "message" => "backend request timed out: #{command}"
66
+ }
67
+ ensure
68
+ @pending_mutex.synchronize { @pending.delete(seq) }
69
+ end
70
+
71
+ def stop!
72
+ @stdin&.close
73
+ @stdout&.close
74
+ @stderr&.close
75
+ if @wait_thread&.alive?
76
+ Process.kill("TERM", @wait_thread.pid)
77
+ end
78
+ rescue StandardError
79
+ nil
80
+ ensure
81
+ @wait_thread&.join(0.2)
82
+ @reader_thread&.join(0.2)
83
+ @stderr_thread&.join(0.2)
84
+ @stdin = nil
85
+ @stdout = nil
86
+ @stderr = nil
87
+ @wait_thread = nil
88
+ @reader_thread = nil
89
+ @stderr_thread = nil
90
+ end
91
+
92
+ def next_seq
93
+ seq = @next_seq
94
+ @next_seq += 1
95
+ seq
96
+ end
97
+
98
+ def read_loop
99
+ loop do
100
+ message = @protocol.read_message
101
+ break if message.nil?
102
+
103
+ if message["type"] == "response"
104
+ queue = @pending_mutex.synchronize { @pending[message["request_seq"]] }
105
+ queue&.push(message)
106
+ elsif message["type"] == "event"
107
+ @on_event&.call(message)
108
+ elsif message["type"] == "request"
109
+ handle_adapter_request(message)
110
+ end
111
+ end
112
+ rescue StandardError
113
+ nil
114
+ ensure
115
+ @pending_mutex.synchronize do
116
+ @pending.each_value do |queue|
117
+ queue.push({
118
+ "type" => "response",
119
+ "success" => false,
120
+ "message" => "backend closed"
121
+ })
122
+ end
123
+ @pending.clear
124
+ end
125
+ end
126
+
127
+ def drain_stderr
128
+ return unless @stderr
129
+
130
+ @stderr.each_line do |_line|
131
+ # Intentionally ignored in this bridge layer.
132
+ end
133
+ rescue StandardError
134
+ nil
135
+ end
136
+
137
+ def handle_adapter_request(message)
138
+ response = @on_request&.call(message)
139
+ response ||= {
140
+ "type" => "response",
141
+ "request_seq" => message["seq"],
142
+ "success" => false,
143
+ "command" => message["command"],
144
+ "message" => "unsupported reverse request: #{message['command']}"
145
+ }
146
+
147
+ @write_mutex.synchronize do
148
+ @protocol.write_message(response)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -22,7 +22,7 @@ module MilkTea
22
22
  @lldb_backend = if @backend_factory
23
23
  build_backend_via_factory(adapter_command)
24
24
  else
25
- Backends::LLDBDAP.new(
25
+ LLDBDAPBackend.new(
26
26
  adapter_command: adapter_command,
27
27
  on_event: method(:handle_backend_event),
28
28
  on_request: method(:handle_backend_request)
data/lib/milk_tea/dap.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "dap/protocol"
4
- require_relative "dap/backends/lldb_dap"
4
+ require_relative "dap/lldb_dap_backend"
5
5
  require_relative "dap/session"
6
6
  require_relative "dap/server"
@@ -556,9 +556,15 @@ module MilkTea
556
556
  next_tok&.type == :colon
557
557
  end
558
558
 
559
+ # Keywords whose standalone syntax is "keyword:" — they form blocks or
560
+ # expressions directly with a colon and no intervening token, so they are
561
+ # not field declarations when followed by ":".
562
+ STANDS_ALONE_WITH_COLON = %i[unsafe else defer parallel].to_set.freeze
563
+
559
564
  def keyword_field_declaration_token?(tokens, index)
560
565
  return false if match_arm_binding_token?(tokens, index)
561
566
  return false if destructure_let_binding?(tokens, index)
567
+ return false if STANDS_ALONE_WITH_COLON.include?(tokens[index].type)
562
568
 
563
569
  next_tok = next_non_trivia_token(tokens, index + 1)
564
570
  next_tok&.type == :colon
data/lib/milk_tea/lsp.rb CHANGED
@@ -1,13 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module MilkTea
4
- module LSP
5
- # LSP implementation - language server protocol support
6
- end
7
- end
8
-
9
3
  require_relative 'lsp/protocol'
10
4
  require_relative 'lsp/dependency_resolution'
11
5
  require_relative 'lsp/workspace'
12
6
  require_relative 'lsp/diagnostics'
13
- require_relative 'lsp/server'
7
+ require_relative 'lsp/server'
@@ -118,4 +118,4 @@ module MilkTea
118
118
  nil
119
119
  end
120
120
  end
121
- end
121
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandBindgen
6
+ def bindgen_command
7
+ BindgenCLI.start(@argv, out: @out, err: @err, help_printer: method(:print_bindgen_help))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandBuild
6
+ def build_command
7
+ path, options = extract_path_and_options(allow_clean: true)
8
+ return 1 unless path
9
+
10
+ if options.delete(:clean)
11
+ cleaned_path = Build.clean(path, output_path: options[:output_path], profile: options[:profile], platform: options[:platform], bundle: options[:bundle], archive: options[:archive])
12
+ info("cleaned #{cleaned_path}")
13
+ return 0
14
+ end
15
+
16
+ frozen = options.delete(:frozen)
17
+ ensure_current_lockfile!(path) if frozen
18
+ locked = options.delete(:locked)
19
+ bundle = options[:bundle]
20
+ package_graph = package_graph_for(path, locked:)
21
+ result = Build.build(path, module_roots: module_roots_for(path, locked:), package_graph:, frontend: @build_frontend, **options.except(:timings))
22
+ if bundle
23
+ info("built #{path} -> #{File.dirname(result.output_path)}")
24
+ info("entry executable #{result.output_path}")
25
+ info(" [cached]") if result.cached
26
+ info("archive #{result.archive_path}") if result.archive_path
27
+ elsif result.cached
28
+ info("built #{path} -> #{result.output_path} [cached]")
29
+ else
30
+ info("built #{path} -> #{result.output_path}")
31
+ end
32
+ info("saved C to #{result.c_path}") if result.c_path
33
+ 0
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandCache
6
+ def cache_command
7
+ subcommand = @argv.shift
8
+ unless subcommand
9
+ @err.puts("missing cache subcommand")
10
+ print_command_help("cache", @err)
11
+ return 1
12
+ end
13
+
14
+ cache_root = MilkTea.data_root.join("tmp", "mtc-cache")
15
+
16
+ case subcommand
17
+ when "purge"
18
+ if File.directory?(cache_root)
19
+ FileUtils.rm_rf(cache_root)
20
+ @out.puts("purged #{cache_root}")
21
+ else
22
+ @out.puts("cache is already empty")
23
+ end
24
+ 0
25
+ when "status"
26
+ unless File.directory?(cache_root)
27
+ @out.puts("cache directory does not exist: #{cache_root}")
28
+ return 0
29
+ end
30
+ program_dirs = Dir.glob(File.join(cache_root, "programs", "*", "*")).select { |d| File.directory?(d) }
31
+ binary_files = Dir.glob(File.join(cache_root, "binaries", "*", "*", "binary")).select { |f| File.file?(f) }
32
+ total_size = (program_dirs + binary_files).sum { |p|
33
+ File.file?(p) ? File.size(p) : Dir.glob(File.join(p, "**", "*")).sum { |f| File.file?(f) ? File.size(f) : 0 }
34
+ }
35
+ @out.puts("cache #{program_dirs.length} programs, #{binary_files.length} binaries (#{format_size(total_size)})")
36
+ @out.puts(" root #{cache_root}")
37
+ 0
38
+ else
39
+ @err.puts("unknown cache subcommand #{subcommand}")
40
+ print_command_help("cache", @err)
41
+ 1
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandCheck
6
+ def check_command
7
+ args = @argv.dup
8
+ @argv = []
9
+ until args.empty?
10
+ arg = args.shift
11
+ @argv << arg
12
+ end
13
+
14
+ unless @argv.any?
15
+ @err.puts("missing source file path")
16
+ print_usage(@err)
17
+ return 1
18
+ end
19
+
20
+ resolution = extract_resolution_flags!
21
+ input_paths = @argv.dup
22
+ return 1 unless ensure_known_source_operands!("check", input_paths)
23
+
24
+ paths = expand_source_paths(input_paths)
25
+ return 0 if print_no_source_files_if_empty(paths, input_paths)
26
+
27
+ ensure_current_lockfiles!(paths) if resolution[:frozen]
28
+
29
+ all_diagnostics = []
30
+ paths.each do |path|
31
+ diagnostics, module_name, closure_errors = check_single_reporting_all(path, locked: resolution[:locked])
32
+ closure_errors = [] if paths.length > 1
33
+ diagnostics = sort_by_location(diagnostics)
34
+
35
+ if diagnostics.any? || closure_errors.any?
36
+ main_source = read_source_file(path)
37
+ main_abs = File.expand_path(path)
38
+ diagnostics.each do |d|
39
+ same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
40
+ source = same_file ? main_source : nil
41
+ @err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
42
+ end
43
+ closure_errors.each do |d|
44
+ same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
45
+ source = same_file ? main_source : nil
46
+ @err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
47
+ end
48
+ all_diagnostics.concat(diagnostics)
49
+ all_diagnostics.concat(closure_errors)
50
+ elsif module_name
51
+ info("checked #{path} as #{module_name}")
52
+ end
53
+ end
54
+
55
+ return 0 if all_diagnostics.empty?
56
+
57
+ error_count = all_diagnostics.count { |d| !d.respond_to?(:severity) || d.severity == :error }
58
+ warning_count = all_diagnostics.count { |d| d.respond_to?(:severity) && d.severity == :warning }
59
+ info_count = all_diagnostics.count { |d| d.respond_to?(:severity) && (d.severity == :info || d.severity == :hint) }
60
+
61
+ @err.puts
62
+ parts = []
63
+ parts << "#{error_count} #{error_count == 1 ? 'error' : 'errors'}" if error_count > 0
64
+ parts << "#{warning_count} #{warning_count == 1 ? 'warning' : 'warnings'}" if warning_count > 0
65
+ parts << "#{info_count} #{info_count == 1 ? 'note' : 'notes'}" if info_count > 0
66
+ body = parts.join("; ")
67
+ if error_count > 0
68
+ @err.puts("#{body} found")
69
+ elsif warning_count > 0
70
+ @err.puts("#{body}")
71
+ end
72
+ final_error_count = error_count + (resolution[:warnings_as_errors] ? warning_count : 0)
73
+ final_error_count > 0 ? 1 : 0
74
+ end
75
+
76
+ def check_single_reporting_all(path, locked: false)
77
+ loader = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform)
78
+ resolved_path = File.expand_path(path)
79
+ ast = loader.load_file(resolved_path)
80
+ module_name = ast.module_name.to_s
81
+
82
+ import_result = loader.imported_modules_for_ast_collecting_errors(ast, importer_path: resolved_path)
83
+ errors = import_result.errors.dup
84
+
85
+ analysis = nil
86
+ begin
87
+ result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path)
88
+ errors.concat(result[:errors])
89
+ analysis = result[:analysis]
90
+ rescue SemanticError => e
91
+ errors << e
92
+ end
93
+
94
+ if analysis && errors.empty?
95
+ source = read_source_file(path)
96
+ warnings = Linter.lint_source(source, path: resolved_path, sema_facts: analysis, lint_tier: :full)
97
+ errors.concat(warnings)
98
+ end
99
+
100
+ closure_errors = loader.collecting_path_errors.values.flatten.compact
101
+ [errors, module_name, closure_errors]
102
+ rescue ModuleLoadError, PackageLockError, SemanticError => e
103
+ [[e], nil, []]
104
+ end
105
+
106
+ def sort_by_location(errors)
107
+ errors.sort_by do |e|
108
+ actual = e.respond_to?(:error) ? e.error : e
109
+ line = actual.respond_to?(:line) ? actual.line.to_i : 0
110
+ column = actual.respond_to?(:column) ? actual.column.to_i : 0
111
+ [line, column]
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandBase
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandCompletions
6
+ def completions_command
7
+ shell = @argv.shift
8
+ unless %w[bash zsh fish].include?(shell)
9
+ @err.puts("completions: shell must be bash, zsh, or fish")
10
+ print_command_help("completions", @err)
11
+ return 1
12
+ end
13
+
14
+ @out.puts(completion_script(shell))
15
+ 0
16
+ end
17
+
18
+ def completion_script(shell)
19
+ names = COMMANDS.map(&:first)
20
+ case shell
21
+ when "bash"
22
+ [
23
+ "# mtc bash completion. Source this file or install it into your bash",
24
+ "# completion directory (e.g. /etc/bash_completion.d/mtc).",
25
+ "_mtc() {",
26
+ %( local cur="${COMP_WORDS[COMP_CWORD]}"),
27
+ %( if [ "${COMP_CWORD}" -eq 1 ]; then),
28
+ %( COMPREPLY=( $(compgen -W "#{(names + %w[help version]).join(' ')}" -- "${cur}") )),
29
+ " fi",
30
+ "}",
31
+ "complete -F _mtc mtc",
32
+ ].join("\n")
33
+ when "zsh"
34
+ lines = ["#compdef mtc", "# mtc zsh completion. Install onto your $fpath as _mtc.", "_mtc() {", " local -a commands", " commands=("]
35
+ COMMANDS.each { |name, summary| lines << " '#{name}:#{summary}'" }
36
+ lines.concat([" )", " if (( CURRENT == 2 )); then", " _describe 'mtc command' commands", " fi", "}", %(_mtc "$@")])
37
+ lines.join("\n")
38
+ when "fish"
39
+ lines = ["# mtc fish completion. Install into ~/.config/fish/completions/mtc.fish."]
40
+ COMMANDS.each do |name, summary|
41
+ lines << "complete -c mtc -f -n '__fish_use_subcommand' -a #{name} -d '#{summary}'"
42
+ end
43
+ lines.join("\n")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end