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.
- checksums.yaml +4 -4
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
- data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
- data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
- data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
- data/lib/milk_tea/core/c_backend.rb +49 -39
- data/lib/milk_tea/core/compile_time.rb +98 -72
- data/lib/milk_tea/core/intrinsics.rb +7 -0
- data/lib/milk_tea/core/lexer.rb +46 -35
- data/lib/milk_tea/core/lowering/block.rb +9 -0
- data/lib/milk_tea/core/lowering/calls.rb +2 -0
- data/lib/milk_tea/core/lowering/declarations.rb +1 -1
- data/lib/milk_tea/core/lowering/functions.rb +11 -8
- data/lib/milk_tea/core/lowering/resolve.rb +10 -3
- data/lib/milk_tea/core/lowering/scans.rb +13 -18
- data/lib/milk_tea/core/module_binder.rb +9 -10
- data/lib/milk_tea/core/module_loader.rb +38 -42
- data/lib/milk_tea/core/module_path_resolver.rb +1 -4
- data/lib/milk_tea/core/parser/declarations.rb +43 -19
- data/lib/milk_tea/core/parser/expressions.rb +4 -7
- data/lib/milk_tea/core/parser/statements.rb +5 -5
- data/lib/milk_tea/core/parser.rb +26 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +117 -85
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
- data/lib/milk_tea/core/semantic_analyzer.rb +56 -37
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
- data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
- data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
- data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
- data/lib/milk_tea/tooling/cli/commands/check.rb +116 -0
- data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
- data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
- data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
- data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
- data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
- data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
- data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
- data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
- data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
- data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
- data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
- data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
- data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
- data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
- data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
- data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
- data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
- data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
- data/lib/milk_tea/tooling/cli.rb +101 -1893
- 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
|