mt-lang 0.3.17 → 0.3.22

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/milk_tea/base.rb +1 -1
  3. data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
  4. data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
  5. data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
  6. data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
  7. data/lib/milk_tea/core/c_backend.rb +49 -39
  8. data/lib/milk_tea/core/compile_time.rb +98 -72
  9. data/lib/milk_tea/core/control_flow/builder.rb +2 -1
  10. data/lib/milk_tea/core/intrinsics.rb +7 -0
  11. data/lib/milk_tea/core/lexer.rb +46 -35
  12. data/lib/milk_tea/core/lowering/block.rb +9 -0
  13. data/lib/milk_tea/core/lowering/calls.rb +2 -0
  14. data/lib/milk_tea/core/lowering/declarations.rb +1 -1
  15. data/lib/milk_tea/core/lowering/functions.rb +11 -8
  16. data/lib/milk_tea/core/lowering/resolve.rb +11 -4
  17. data/lib/milk_tea/core/lowering/scans.rb +13 -18
  18. data/lib/milk_tea/core/module_binder.rb +19 -15
  19. data/lib/milk_tea/core/module_loader.rb +212 -59
  20. data/lib/milk_tea/core/module_path_resolver.rb +8 -7
  21. data/lib/milk_tea/core/parser/declarations.rb +43 -19
  22. data/lib/milk_tea/core/parser/expressions.rb +5 -8
  23. data/lib/milk_tea/core/parser/statements.rb +5 -5
  24. data/lib/milk_tea/core/parser.rb +26 -0
  25. data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
  26. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
  27. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +146 -92
  28. data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
  29. data/lib/milk_tea/core/semantic_analyzer.rb +56 -37
  30. data/lib/milk_tea/core/types.rb +12 -17
  31. data/lib/milk_tea/lsp/diagnostics.rb +13 -0
  32. data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
  33. data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
  34. data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
  35. data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
  36. data/lib/milk_tea/tooling/cli/commands/check.rb +106 -0
  37. data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
  38. data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
  39. data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
  40. data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
  41. data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
  42. data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
  43. data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
  44. data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
  45. data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
  46. data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
  47. data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
  48. data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
  49. data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
  50. data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
  51. data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
  52. data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
  53. data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
  54. data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
  55. data/lib/milk_tea/tooling/cli.rb +101 -1893
  56. data/lib/milk_tea/tooling/sexpr_dumper.rb +7 -7
  57. metadata +24 -2
@@ -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
@@ -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
 
@@ -822,6 +822,18 @@ module MilkTea
822
822
  self
823
823
  end
824
824
 
825
+ def eql?(other)
826
+ other.is_a?(GenericStructDefinition) && other.name == name &&
827
+ other.type_params == type_params && other.module_name == module_name &&
828
+ other.external == external && other.packed == packed && other.alignment == alignment
829
+ end
830
+
831
+ alias == eql?
832
+
833
+ def hash
834
+ [self.class, name, type_params, module_name, external, packed, alignment].hash
835
+ end
836
+
825
837
  def set_layout(packed:, alignment:)
826
838
  @packed = packed
827
839
  @alignment = alignment
@@ -853,23 +865,6 @@ module MilkTea
853
865
  name
854
866
  end
855
867
 
856
- def eql?(other)
857
- other.class == self.class &&
858
- other.name == name &&
859
- other.type_params == type_params &&
860
- other.module_name == module_name &&
861
- other.external == external &&
862
- other.packed == packed &&
863
- other.alignment == alignment &&
864
- other.linkage_name == linkage_name
865
- end
866
-
867
- alias == eql?
868
-
869
- def hash
870
- [self.class, name, type_params, module_name, external, packed, alignment, linkage_name].hash
871
- end
872
-
873
868
  def instantiate(arguments)
874
869
  raise ArgumentError, "#{name} expects #{type_params.length} type arguments, got #{arguments.length}" unless arguments.length == type_params.length
875
870
 
@@ -179,6 +179,19 @@ module MilkTea
179
179
  platform: effective_platform,
180
180
  )
181
181
  module_name = loader.send(:inferred_module_name_for_path, path) rescue nil
182
+
183
+ # Run the full two-pass program check first so that @analysis_cache
184
+ # has fully-checked analyses for all transitive modules. When the
185
+ # import resolution below encounters a cycle member, it finds the
186
+ # cached analysis and resolves correctly. If the program check fails
187
+ # (e.g. missing module), fall through to the standard resolution
188
+ # path so that prelude modules and regular errors are still reported.
189
+ begin
190
+ loader.check_program_collecting(path)
191
+ rescue ModuleLoadError, PackageLockError
192
+ # best-effort — standard path below handles diagnostics
193
+ end
194
+
182
195
  resolution_result = loader.imported_modules_for_ast_collecting_errors(ast, importer_path: path)
183
196
  unresolved_import_paths = []
184
197
 
@@ -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
@@ -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,106 @@
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
+
80
+ result = loader.check_program_collecting(path)
81
+ errors = result[:errors]
82
+ analysis = result[:root_analysis]
83
+ module_name = result[:module_name]
84
+
85
+ if analysis && errors.empty?
86
+ source = read_source_file(path)
87
+ warnings = Linter.lint_source(source, path: resolved_path, sema_facts: analysis, lint_tier: :full)
88
+ errors.concat(warnings)
89
+ end
90
+
91
+ [errors, module_name, []]
92
+ rescue ModuleLoadError, PackageLockError => e
93
+ [[e], nil, []]
94
+ end
95
+
96
+ def sort_by_location(errors)
97
+ errors.sort_by do |e|
98
+ actual = e.respond_to?(:error) ? e.error : e
99
+ line = actual.respond_to?(:line) ? actual.line.to_i : 0
100
+ column = actual.respond_to?(:column) ? actual.column.to_i : 0
101
+ [line, column]
102
+ end
103
+ end
104
+ end
105
+ end
106
+ 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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDap
6
+ def dap_command
7
+ preferred_backend_kind = "process"
8
+ adapter_command = nil
9
+
10
+ until @argv.empty?
11
+ arg = @argv.shift
12
+ case arg
13
+ when "--log-level"
14
+ @argv.shift
15
+ when /\A--log-level=(.+)\z/
16
+ # accept and ignore
17
+ when "--backend"
18
+ preferred_backend_kind = @argv.shift&.downcase
19
+ when /\A--backend=(.+)\z/
20
+ preferred_backend_kind = ::Regexp.last_match(1).downcase
21
+ when "--adapter-path"
22
+ adapter_path = @argv.shift
23
+ adapter_command = resolve_adapter_path(adapter_path)
24
+ return 1 unless adapter_command
25
+ when /\A--adapter-path=(.+)\z/
26
+ adapter_path = ::Regexp.last_match(1)
27
+ adapter_command = resolve_adapter_path(adapter_path)
28
+ return 1 unless adapter_command
29
+ else
30
+ if arg.start_with?("-")
31
+ @err.puts("dap: unknown option #{arg}")
32
+ return 1
33
+ end
34
+ @err.puts("dap: unexpected argument #{arg}")
35
+ return 1
36
+ end
37
+ end
38
+
39
+ require "milk_tea/dap/server" unless defined?(MilkTea::DAP::Server)
40
+ server = MilkTea::DAP::Server.new(
41
+ preferred_backend_kind:,
42
+ adapter_command:,
43
+ )
44
+ server.run
45
+ 0
46
+ end
47
+
48
+ def resolve_adapter_path(adapter_path)
49
+ unless adapter_path && File.file?(adapter_path)
50
+ @err.puts("dap: adapter path not found: #{adapter_path}")
51
+ return nil
52
+ end
53
+ expanded = File.expand_path(adapter_path)
54
+ adapter_path.end_with?(".rb") ? [RbConfig.ruby, expanded] : [expanded]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDebug
6
+ def debug_command
7
+ unless @argv.any?
8
+ @err.puts("missing source file path")
9
+ print_usage(@err)
10
+ return 1
11
+ end
12
+
13
+ resolution = extract_resolution_flags!
14
+ input_paths = @argv.dup
15
+ return 1 unless ensure_known_source_operands!("debug", input_paths)
16
+
17
+ path = expand_source_paths(input_paths).first
18
+ unless path
19
+ @err.puts("no .mt files found in #{input_paths.join(', ')}")
20
+ return 1
21
+ end
22
+
23
+ ensure_current_lockfile!(path) if resolution[:frozen]
24
+
25
+ source = read_source_file(path)
26
+ resolved_path = File.expand_path(path)
27
+
28
+ tokens = MilkTea::Lexer.lex(source, path: resolved_path)
29
+
30
+ parse_result = MilkTea::Parser.parse_collecting_errors(source, path: resolved_path)
31
+ ast = parse_result.ast
32
+ parse_errors = parse_result.errors.dup
33
+
34
+ facts = nil
35
+ snapshot = nil
36
+ loader_ast = ast
37
+
38
+ if ast && parse_errors.empty?
39
+ begin
40
+ loader = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform)
41
+ loader_ast = loader.load_file(resolved_path)
42
+
43
+ import_result = loader.send(:imported_modules_for_ast_collecting_errors, loader_ast, importer_path: resolved_path)
44
+ import_errors = import_result.respond_to?(:errors) ? import_result.errors : []
45
+ parse_errors.concat(import_errors) unless import_errors.empty?
46
+
47
+ snapshot = MilkTea::SemanticAnalyzer.tooling_snapshot(
48
+ loader_ast,
49
+ imported_modules: import_result.modules,
50
+ allow_missing_imports: true,
51
+ path: resolved_path,
52
+ )
53
+ facts = snapshot&.facts
54
+ rescue MilkTea::LexError, MilkTea::ParseError, ModuleLoadError, SemanticError => e
55
+ parse_errors << e
56
+ end
57
+ end
58
+
59
+ text = DebugInfoFormatter.format_all(
60
+ content: source,
61
+ tokens: tokens,
62
+ ast: loader_ast,
63
+ parse_errors: parse_errors,
64
+ facts: facts,
65
+ snapshot: snapshot,
66
+ path: resolved_path,
67
+ )
68
+
69
+ @out.puts(text)
70
+ 0
71
+ rescue MilkTea::LexError => e
72
+ @err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
73
+ 1
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDeps
6
+ def deps_command
7
+ PackageManagerCLI.start(
8
+ @argv,
9
+ out: @out,
10
+ err: @err,
11
+ help_printer: method(:print_deps_help),
12
+ services: package_services,
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDocs
6
+ def docs_command
7
+ port = nil
8
+ open_flag = false
9
+
10
+ while (arg = @argv.first)
11
+ case arg
12
+ when "--port", "-p"
13
+ @argv.shift
14
+ port = @argv.shift.to_i
15
+ port = nil if port <= 0 || port > 65535
16
+ when "--open", "-o"
17
+ open_flag = true
18
+ @argv.shift
19
+ else
20
+ break
21
+ end
22
+ end
23
+
24
+ port = resolve_docs_port(port)
25
+
26
+ DocsApp.set :port, port
27
+ DocsApp.set :bind, "127.0.0.1"
28
+ DocsApp.set :environment, :production
29
+ DocsApp.set :server, :puma
30
+
31
+ url = "http://127.0.0.1:#{port}/"
32
+
33
+ @out.puts("Serving Milk Tea docs at #{url}")
34
+ @out.puts("Press Ctrl+C to stop.")
35
+
36
+ if open_flag
37
+ open_browser(url)
38
+ end
39
+
40
+ DocsApp.run!
41
+ 0
42
+ rescue Interrupt
43
+ 0
44
+ end
45
+
46
+ def resolve_docs_port(preferred)
47
+ return preferred if preferred
48
+
49
+ server = TCPServer.new("127.0.0.1", 0)
50
+ port = server.addr[1]
51
+ server.close
52
+ port
53
+ rescue StandardError
54
+ 4567
55
+ end
56
+ end
57
+ end
58
+ end