mt-lang 0.3.17 → 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 (53) 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/intrinsics.rb +7 -0
  10. data/lib/milk_tea/core/lexer.rb +46 -35
  11. data/lib/milk_tea/core/lowering/block.rb +9 -0
  12. data/lib/milk_tea/core/lowering/calls.rb +2 -0
  13. data/lib/milk_tea/core/lowering/declarations.rb +1 -1
  14. data/lib/milk_tea/core/lowering/functions.rb +11 -8
  15. data/lib/milk_tea/core/lowering/resolve.rb +10 -3
  16. data/lib/milk_tea/core/lowering/scans.rb +13 -18
  17. data/lib/milk_tea/core/module_binder.rb +9 -10
  18. data/lib/milk_tea/core/module_loader.rb +38 -42
  19. data/lib/milk_tea/core/module_path_resolver.rb +1 -4
  20. data/lib/milk_tea/core/parser/declarations.rb +43 -19
  21. data/lib/milk_tea/core/parser/expressions.rb +4 -7
  22. data/lib/milk_tea/core/parser/statements.rb +5 -5
  23. data/lib/milk_tea/core/parser.rb +26 -0
  24. data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
  25. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
  26. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +117 -85
  27. data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
  28. data/lib/milk_tea/core/semantic_analyzer.rb +56 -37
  29. data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
  30. data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
  31. data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
  32. data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
  33. data/lib/milk_tea/tooling/cli/commands/check.rb +116 -0
  34. data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
  35. data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
  36. data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
  37. data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
  38. data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
  39. data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
  40. data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
  41. data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
  42. data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
  43. data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
  44. data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
  45. data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
  46. data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
  47. data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
  48. data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
  49. data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
  50. data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
  51. data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
  52. data/lib/milk_tea/tooling/cli.rb +101 -1893
  53. metadata +24 -2
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandFormat
6
+ def format_command
7
+ parsed = parse_format_options
8
+ return 1 unless parsed
9
+
10
+ options = parsed[:options]
11
+ input_paths = parsed[:input_paths]
12
+
13
+ if input_paths.empty?
14
+ @err.puts("missing source file path")
15
+ print_usage(@err)
16
+ return 1
17
+ end
18
+
19
+ paths = expand_source_paths(input_paths)
20
+ return 0 if print_no_source_files_if_empty(paths, input_paths)
21
+
22
+ multiple_sources = input_paths.length > 1 || input_paths.any? { |path| File.directory?(path) }
23
+ if multiple_sources
24
+ unless options[:check] || options[:write]
25
+ @err.puts("format on multiple sources requires --check or --write")
26
+ print_usage(@err)
27
+ return 1
28
+ end
29
+
30
+ return format_paths(paths, options)
31
+ end
32
+
33
+ path = paths.first
34
+
35
+ source = read_source_file(path)
36
+ format_profile = options[:profile] ? Linter::Profile.new : nil
37
+ start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
38
+ result = Formatter.check_source(source, path: path, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
39
+ elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
40
+
41
+ rc = if options[:check]
42
+ announce_file_action(path, "format-check")
43
+ if result.changed
44
+ info("needs formatting #{path}")
45
+ 1
46
+ else
47
+ info("already formatted #{path}")
48
+ 0
49
+ end
50
+ elsif options[:write]
51
+ announce_file_action(path, "format-write")
52
+ if result.changed
53
+ File.write(path, result.formatted_source)
54
+ info("formatted #{path}")
55
+ else
56
+ info("already formatted #{path}")
57
+ end
58
+ 0
59
+ else
60
+ @out.write(result.formatted_source)
61
+ 0
62
+ end
63
+
64
+ print_file_profiles([{ path:, total_ms: elapsed_ms, profile: format_profile }], "format") if options[:profile]
65
+ rc
66
+ end
67
+
68
+ def format_paths(paths, options)
69
+ format_profiles = []
70
+ if options[:check]
71
+ needs_fmt = []
72
+ paths.each do |p|
73
+ announce_file_action(p, "format-check")
74
+ format_profile = options[:profile] ? Linter::Profile.new : nil
75
+ start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
76
+ result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
77
+ elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
78
+ format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
79
+ needs_fmt << p if result.changed
80
+ end
81
+ print_file_profiles(format_profiles, "format") if options[:profile]
82
+ if needs_fmt.empty?
83
+ info("all #{paths.size} file(s) already formatted")
84
+ return 0
85
+ end
86
+ needs_fmt.each { |p| info("needs formatting #{p}") }
87
+ info("#{needs_fmt.size} file(s) need formatting")
88
+ return 1
89
+ end
90
+
91
+ # --write
92
+ changed = 0
93
+ paths.each do |p|
94
+ announce_file_action(p, "format-write")
95
+ format_profile = options[:profile] ? Linter::Profile.new : nil
96
+ start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
97
+ result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
98
+ elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
99
+ format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
100
+ if result.changed
101
+ File.write(p, result.formatted_source)
102
+ info("formatted #{p}")
103
+ changed += 1
104
+ end
105
+ end
106
+ print_file_profiles(format_profiles, "format") if options[:profile]
107
+ info("formatted #{changed} of #{paths.size} file(s)")
108
+ 0
109
+ end
110
+
111
+ def parse_format_options
112
+ options = {
113
+ check: false,
114
+ write: false,
115
+ mode: :safe,
116
+ max_line_length: nil,
117
+ profile: false,
118
+ }
119
+ input_paths = []
120
+
121
+ until @argv.empty?
122
+ option = @argv.shift
123
+ if option.start_with?("-")
124
+ case option
125
+ when "--check"
126
+ options[:check] = true
127
+ when "--write", "-w"
128
+ options[:write] = true
129
+ when "--preserve"
130
+ options[:mode] = :preserve
131
+ when "--canonical"
132
+ options[:mode] = :canonical
133
+ when "--safe"
134
+ options[:mode] = :safe
135
+ when "--tidy"
136
+ options[:mode] = :tidy
137
+ when "--max-line-length"
138
+ value = @argv.shift
139
+ return missing_option_value(option) unless value
140
+
141
+ line_length = Integer(value, exception: false)
142
+ unless line_length && line_length.positive?
143
+ @err.puts("--max-line-length must be a positive integer")
144
+ print_usage(@err)
145
+ return nil
146
+ end
147
+
148
+ options[:max_line_length] = line_length
149
+ when "--timings"
150
+ options[:profile] = true
151
+ when "--"
152
+ input_paths.concat(@argv)
153
+ @argv.clear
154
+ else
155
+ @err.puts("unknown format option #{option}")
156
+ print_usage(@err)
157
+ return nil
158
+ end
159
+ else
160
+ input_paths << option
161
+ end
162
+ end
163
+
164
+ if options[:check] && options[:write]
165
+ @err.puts("format options --check and --write cannot be combined")
166
+ print_usage(@err)
167
+ return nil
168
+ end
169
+
170
+ { options:, input_paths: }
171
+ end
172
+
173
+ def print_file_profiles(file_profiles, label)
174
+ sorted = file_profiles.sort_by { |fp| -fp[:total_ms] }
175
+ return if sorted.empty?
176
+
177
+ @out.puts
178
+ if sorted.size == 1
179
+ entry = sorted.first
180
+ phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
181
+ phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 0.1 }&.join(", ")
182
+ detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
183
+ @out.puts("#{label} profile #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
184
+ return
185
+ end
186
+
187
+ @out.puts("Profile (#{label}): #{sorted.size} file(s)")
188
+ sorted.each do |entry|
189
+ phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
190
+ phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 1.0 }&.join(", ")
191
+ detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
192
+ @out.puts(" #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
193
+ end
194
+ total = sorted.sum { |fp| fp[:total_ms] }
195
+ @out.puts("Total: #{format('%.1f', total)}ms")
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandLex
6
+ def lex_command
7
+ path = nil
8
+ sexpr = false
9
+
10
+ args = @argv.dup
11
+ @argv = []
12
+ until args.empty?
13
+ arg = args.shift
14
+ next if arg == "--"
15
+
16
+ if arg == "--sexpr"
17
+ sexpr = true
18
+ next
19
+ end
20
+
21
+ if path.nil?
22
+ path = arg
23
+ else
24
+ @err.puts("unknown option: #{arg}")
25
+ print_usage(@err)
26
+ return 1
27
+ end
28
+ end
29
+
30
+ unless path
31
+ @err.puts("missing source file path")
32
+ print_usage(@err)
33
+ return 1
34
+ end
35
+
36
+ tokens = Lexer.lex(read_source_file(path), path: path)
37
+ if sexpr
38
+ @out.puts(SexprDumper.dump_tokens(tokens))
39
+ else
40
+ @out.write(PP.pp(tokens, +""))
41
+ end
42
+ 0
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandLint
6
+ def lint_command
7
+ resolution = { locked: false, frozen: false }
8
+ select = nil
9
+ ignore = nil
10
+ fix = false
11
+ init = false
12
+ ignore_generated = false
13
+ profile = false
14
+ input_paths = []
15
+ until @argv.empty?
16
+ arg = @argv.shift
17
+ unless arg.start_with?("--")
18
+ input_paths << arg
19
+ next
20
+ end
21
+
22
+ flag = arg
23
+ case flag
24
+ when "--select"
25
+ arg = @argv.shift
26
+ unless arg
27
+ @err.puts("--select requires a comma-separated list of rule codes")
28
+ return 1
29
+ end
30
+ select = arg.split(",").map(&:strip).to_set
31
+ when "--ignore"
32
+ arg = @argv.shift
33
+ unless arg
34
+ @err.puts("--ignore requires a comma-separated list of rule codes")
35
+ return 1
36
+ end
37
+ ignore = arg.split(",").map(&:strip).to_set
38
+ when "--fix"
39
+ fix = true
40
+ when "--init"
41
+ init = true
42
+ when "--locked"
43
+ resolution[:locked] = true
44
+ when "--frozen"
45
+ resolution[:locked] = true
46
+ resolution[:frozen] = true
47
+ when "--ignore-generated"
48
+ ignore_generated = true
49
+ when "--timings"
50
+ profile = true
51
+ when "--"
52
+ input_paths.concat(@argv)
53
+ @argv.clear
54
+ else
55
+ @err.puts("unknown lint flag: #{flag}")
56
+ return 1
57
+ end
58
+ end
59
+
60
+ if init
61
+ if input_paths.empty? && !select && !ignore && !fix && !resolution[:locked] && !resolution[:frozen]
62
+ return init_lint_config
63
+ end
64
+
65
+ @err.puts("--init does not accept source paths or lint options")
66
+ return 1
67
+ end
68
+
69
+ if input_paths.empty?
70
+ @err.puts("missing source file path")
71
+ print_usage(@err)
72
+ return 1
73
+ end
74
+
75
+ paths = input_paths.flat_map do |path|
76
+ if File.directory?(path)
77
+ Dir.glob(File.join(path, "**/*.mt")).sort
78
+ else
79
+ [path]
80
+ end
81
+ end.uniq
82
+
83
+ if paths.empty?
84
+ label = input_paths.length == 1 ? input_paths.first : input_paths.join(", ")
85
+ @out.puts("no .mt files found in #{label}")
86
+ return 0
87
+ end
88
+
89
+ ensure_current_lockfiles!(paths) if resolution[:frozen]
90
+
91
+ if fix
92
+ lint_profiles = []
93
+ paths.each do |p|
94
+ announce_file_action(p, "lint-fix")
95
+ source = read_source_file(p)
96
+ if ignore_generated && generated_source?(source)
97
+ @out.puts("ignored generated #{p}")
98
+ next
99
+ end
100
+
101
+ facts = lint_sema_facts_for(source, p, locked: resolution[:locked])
102
+ prof = profile ? Linter::Profile.new : nil
103
+ start_time = profile ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
104
+
105
+ fixed = Linter.fix_source(
106
+ source,
107
+ path: p,
108
+ sema_facts: facts,
109
+ select:,
110
+ ignore:,
111
+ profile: prof,
112
+ )
113
+ total_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
114
+ lint_profiles << { path: p, profile: prof, mode: :pre_fix_scan, total_ms: } if prof
115
+ if fixed != source
116
+ File.write(p, fixed)
117
+ @out.puts("fixed #{p}")
118
+ end
119
+ end
120
+ print_lint_rule_profiles(lint_profiles) if profile
121
+ print_lint_file_profiles(lint_profiles) if profile
122
+ return 0
123
+ end
124
+
125
+ lint_profiles = []
126
+ all_warnings = paths.flat_map do |p|
127
+ announce_file_action(p, "lint")
128
+ source = read_source_file(p)
129
+ next [] if ignore_generated && generated_source?(source)
130
+
131
+ facts = lint_sema_facts_for(source, p, locked: resolution[:locked])
132
+ prof = profile ? Linter::Profile.new : nil
133
+ start_time = profile ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
134
+ warnings = Linter.lint_source(source, path: p, select:, ignore:, sema_facts: facts, profile: prof)
135
+ total_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
136
+ lint_profiles << { path: p, profile: prof, mode: :lint, total_ms: } if prof
137
+ warnings
138
+ end
139
+
140
+ if all_warnings.empty?
141
+ if input_paths.length == 1
142
+ info("clean #{input_paths.first}")
143
+ else
144
+ info("clean #{paths.size} file(s)")
145
+ end
146
+ print_lint_file_profiles(lint_profiles) if profile
147
+ return 0
148
+ end
149
+
150
+ all_warnings.each do |warning|
151
+ @out.puts("#{warning.path}:#{warning.line}: #{warning.code}: #{warning.message}")
152
+ end
153
+
154
+ print_lint_rule_profiles(lint_profiles) if profile
155
+ print_lint_file_profiles(lint_profiles) if profile
156
+
157
+ file_count = all_warnings.map(&:path).uniq.size
158
+ noun = all_warnings.size == 1 ? "warning" : "warnings"
159
+ files_str = file_count == 1 ? "1 file" : "#{file_count} files"
160
+ @out.puts("Found #{all_warnings.size} #{noun} in #{files_str}.")
161
+ 1
162
+ end
163
+
164
+ def init_lint_config
165
+ path = File.join(Dir.pwd, Linter::DEFAULT_CONFIG_FILE_NAME)
166
+ if File.exist?(path)
167
+ @err.puts("lint config already exists at #{path}")
168
+ return 1
169
+ end
170
+
171
+ File.write(path, Linter.default_config_source)
172
+ info("created #{path}")
173
+ 0
174
+ end
175
+
176
+ def print_lint_rule_profiles(lint_profiles, limit: 12)
177
+ lint_profiles.each do |entry|
178
+ profile = entry[:profile]
179
+ next unless profile
180
+
181
+ rows = profile.rule_breakdown(limit:, min_ms: 0.0)
182
+ rule_total = profile.total_time_ms(prefix: "rule.")
183
+ overall_total = profile.total_time_ms
184
+ mode_label = entry[:mode] == :pre_fix_scan ? "pre-fix scan" : "lint scan"
185
+ @out.puts("lint profile #{entry[:path]} (#{mode_label}): rules=#{format('%.1f', rule_total)}ms total=#{format('%.1f', overall_total)}ms")
186
+
187
+ if rows.empty?
188
+ @out.puts(" no rule timing data captured")
189
+ next
190
+ end
191
+
192
+ rows.each do |row|
193
+ share = rule_total.positive? ? ((row[:total_ms] / rule_total) * 100.0) : 0.0
194
+ @out.puts(
195
+ " #{row[:code]}: #{row[:count]}x total=#{format('%.1f', row[:total_ms])}ms avg=#{format('%.2f', row[:avg_ms])}ms share=#{format('%.1f', share)}%"
196
+ )
197
+ end
198
+
199
+ non_rule_rows = profile.timings_ms
200
+ .filter_map do |name, total_ms|
201
+ next if name.start_with?("rule.")
202
+ next if total_ms < 1.0
203
+
204
+ [name, total_ms]
205
+ end
206
+ .sort_by { |_name, total_ms| -total_ms }
207
+ .first(5)
208
+ .map do |name, total_ms|
209
+ count = profile.counts[name]
210
+ "#{name}:#{count}x/#{format('%.1f', total_ms)}ms"
211
+ end
212
+
213
+ @out.puts(" non-rule hot phases: #{non_rule_rows.join(', ')}") unless non_rule_rows.empty?
214
+ end
215
+ end
216
+
217
+ def print_lint_file_profiles(lint_profiles)
218
+ file_entries = lint_profiles.filter_map do |entry|
219
+ total = entry[:total_ms]
220
+ next unless total
221
+
222
+ phases = entry[:profile]&.timings_ms&.reject { |name, _| name.start_with?("rule.") }&.sort_by { |_, ms| -ms }
223
+ { path: entry[:path], total_ms: total, phases: }
224
+ end
225
+ return if file_entries.empty?
226
+
227
+ sorted = file_entries.sort_by { |e| -e[:total_ms] }
228
+ @out.puts
229
+ @out.puts("Profile (lint): #{sorted.size} file(s)")
230
+ sorted.each do |entry|
231
+ phase_str = entry[:phases]&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 1.0 }&.join(", ")
232
+ detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
233
+ @out.puts(" #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
234
+ end
235
+ total = sorted.sum { |e| e[:total_ms] }
236
+ @out.puts("Total: #{format('%.1f', total)}ms")
237
+ end
238
+
239
+ def lint_sema_facts_for(source, path, locked: false)
240
+ ast = Parser.parse(source, path: path)
241
+ imported_modules = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform).imported_modules_for_ast(ast, importer_path: path)
242
+ SemanticAnalyzer.tooling_snapshot(ast, imported_modules: imported_modules, path: path).facts
243
+ rescue MilkTea::LexError, MilkTea::ParseError, SemanticError, ModuleLoadError
244
+ nil
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandLower
6
+ def lower_command
7
+ sexpr = false
8
+ args = @argv.dup
9
+ @argv = []
10
+ until args.empty?
11
+ arg = args.shift
12
+ if arg == "--sexpr"
13
+ sexpr = true
14
+ next
15
+ end
16
+ @argv << arg
17
+ end
18
+
19
+ unless @argv.any?
20
+ @err.puts("missing source file path")
21
+ print_usage(@err)
22
+ return 1
23
+ end
24
+
25
+ resolution = extract_resolution_flags!
26
+ input_paths = @argv.dup
27
+ return 1 unless ensure_known_source_operands!("lower", input_paths)
28
+
29
+ paths = expand_source_paths(input_paths)
30
+ return 0 if print_no_source_files_if_empty(paths, input_paths)
31
+
32
+ ensure_current_lockfiles!(paths) if resolution[:frozen]
33
+
34
+ multiple = paths.length > 1
35
+ paths.each_with_index do |path, index|
36
+ program = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).check_program(path)
37
+ if multiple
38
+ @out.puts("# --- #{path} ---") unless sexpr
39
+ end
40
+ if sexpr
41
+ @out.puts(SexprDumper.dump_ir(Lowering.lower(program)))
42
+ else
43
+ @out.write(PrettyPrinter.format_ir(Lowering.lower(program)))
44
+ end
45
+ end
46
+ 0
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandLsp
6
+ def lsp_command
7
+ log_level = nil
8
+
9
+ until @argv.empty?
10
+ arg = @argv.shift
11
+ case arg
12
+ when "--log-level"
13
+ log_level = @argv.shift&.downcase
14
+ unless log_level && %w[trace debug info warn error].include?(log_level)
15
+ @err.puts("lsp: invalid --log-level #{log_level.inspect} (expected trace, debug, info, warn, or error)")
16
+ return 1
17
+ end
18
+ when /\A--log-level=(.+)\z/
19
+ log_level = ::Regexp.last_match(1).downcase
20
+ unless %w[trace debug info warn error].include?(log_level)
21
+ @err.puts("lsp: invalid --log-level #{log_level.inspect} (expected trace, debug, info, warn, or error)")
22
+ return 1
23
+ end
24
+ when "--stdio"
25
+ # stdio is the only transport; accept the flag as a no-op
26
+ else
27
+ if arg.start_with?("-")
28
+ @err.puts("lsp: unknown option #{arg}")
29
+ return 1
30
+ end
31
+ @err.puts("lsp: unexpected argument #{arg}")
32
+ return 1
33
+ end
34
+ end
35
+
36
+ require "milk_tea/lsp/server" unless defined?(MilkTea::LSP::Server)
37
+ server = MilkTea::LSP::Server.new
38
+ server.run
39
+ 0
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandNew
6
+ def new_command
7
+ name = @argv.shift
8
+ unless name
9
+ @err.puts("missing project name")
10
+ print_usage(@err)
11
+ return 1
12
+ end
13
+
14
+ if @argv.any?
15
+ @err.puts("unknown new option #{@argv.first}")
16
+ print_usage(@err)
17
+ return 1
18
+ end
19
+
20
+ result = ProjectScaffold.create(name)
21
+ info("created #{result.root_path}")
22
+ 0
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandParse
6
+ def parse_command
7
+ sexpr = false
8
+ args = @argv.dup
9
+ @argv = []
10
+ until args.empty?
11
+ arg = args.shift
12
+ if arg == "--sexpr"
13
+ sexpr = true
14
+ next
15
+ end
16
+ @argv << arg
17
+ end
18
+
19
+ unless @argv.any?
20
+ @err.puts("missing source file path")
21
+ print_usage(@err)
22
+ return 1
23
+ end
24
+
25
+ resolution = extract_resolution_flags!
26
+ input_paths = @argv.dup
27
+ return 1 unless ensure_known_source_operands!("parse", input_paths)
28
+
29
+ paths = expand_source_paths(input_paths)
30
+ return 0 if print_no_source_files_if_empty(paths, input_paths)
31
+
32
+ ensure_current_lockfiles!(paths) if resolution[:frozen]
33
+
34
+ multiple = paths.length > 1
35
+ paths.each_with_index do |path, index|
36
+ ast = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).load_file(path)
37
+ if multiple
38
+ @out.puts("# --- #{path} ---") unless sexpr
39
+ end
40
+ if sexpr
41
+ @out.puts(SexprDumper.dump_ast(ast))
42
+ else
43
+ @out.write(PrettyPrinter.format_ast(ast))
44
+ end
45
+ @out.puts if multiple && index < paths.length - 1
46
+ end
47
+ 0
48
+ end
49
+ end
50
+ end
51
+ end