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.
- checksums.yaml +4 -4
- data/docs/language-design.md +3 -4
- data/docs/language-manual.md +3 -3
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/{binding_types.rb → bindings.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 +63 -54
- data/lib/milk_tea/core/compile_time.rb +279 -74
- data/lib/milk_tea/core/control_flow/constant_propagation.rb +1 -1
- data/lib/milk_tea/core/{compatibility_helpers.rb → intrinsics.rb} +9 -2
- data/lib/milk_tea/core/lexer.rb +55 -48
- 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/lowering.rb +18 -18
- data/lib/milk_tea/core/module_binder.rb +9 -10
- data/lib/milk_tea/core/module_loader.rb +64 -57
- 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 +43 -17
- 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 +57 -38
- data/lib/milk_tea/core.rb +6 -2
- data/lib/milk_tea/dap/lldb_dap_backend.rb +153 -0
- data/lib/milk_tea/dap/server/lldb_backend.rb +1 -1
- data/lib/milk_tea/dap.rb +1 -1
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
- data/lib/milk_tea/lsp.rb +1 -7
- data/lib/milk_tea/{bindings/cli.rb → tooling/bindgen_cli.rb} +1 -1
- 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
- data/lib/milk_tea/tooling.rb +1 -1
- metadata +29 -9
- data/lib/milk_tea/core/compile_time/const_eval.rb +0 -182
- data/lib/milk_tea/core/module_loader/errors.rb +0 -23
- data/lib/milk_tea/dap/backends/lldb_dap.rb +0 -156
- /data/lib/milk_tea/core/lowering/{async.rb → async/frame_builder.rb} +0 -0
|
@@ -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
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class CLI
|
|
5
|
+
module CommandRun
|
|
6
|
+
def run_command
|
|
7
|
+
path, options = extract_path_and_options
|
|
8
|
+
return 1 unless path
|
|
9
|
+
|
|
10
|
+
run_and_print_result(path, options)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def app_command
|
|
14
|
+
options = parse_build_options
|
|
15
|
+
return 1 unless options
|
|
16
|
+
|
|
17
|
+
module_name = @argv.shift
|
|
18
|
+
unless module_name
|
|
19
|
+
@err.puts("missing module name")
|
|
20
|
+
print_usage(@err)
|
|
21
|
+
return 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
path = resolve_app_module(module_name)
|
|
25
|
+
unless path
|
|
26
|
+
@err.puts("run-module module not found: #{module_name}")
|
|
27
|
+
return 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
frozen = options.delete(:frozen)
|
|
31
|
+
ensure_current_lockfile!(path) if frozen
|
|
32
|
+
|
|
33
|
+
run_and_print_result(path, options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_path_and_options(allow_clean: false)
|
|
37
|
+
options = parse_build_options(allow_clean:)
|
|
38
|
+
return nil unless options
|
|
39
|
+
|
|
40
|
+
path = @argv.shift
|
|
41
|
+
unless path
|
|
42
|
+
if File.file?(File.join(Dir.pwd, "package.toml"))
|
|
43
|
+
path = Dir.pwd
|
|
44
|
+
else
|
|
45
|
+
@err.puts("missing source file path")
|
|
46
|
+
print_usage(@err)
|
|
47
|
+
return nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
[path, options]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run_and_print_result(path, options)
|
|
55
|
+
frozen = options.delete(:frozen)
|
|
56
|
+
ensure_current_lockfile!(path) if frozen
|
|
57
|
+
locked = options.delete(:locked)
|
|
58
|
+
package_graph = package_graph_for(path, locked:)
|
|
59
|
+
preview_notice_emitted = false
|
|
60
|
+
preview_started = lambda do |message|
|
|
61
|
+
preview_notice_emitted = true
|
|
62
|
+
@out.write(message)
|
|
63
|
+
@out.flush if @out.respond_to?(:flush)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
result = Run.run(
|
|
67
|
+
path,
|
|
68
|
+
module_roots: module_roots_for(path, locked:),
|
|
69
|
+
package_graph:,
|
|
70
|
+
frontend: @build_frontend,
|
|
71
|
+
preview_started:,
|
|
72
|
+
argv: @argv.dup,
|
|
73
|
+
**options.except(:timings)
|
|
74
|
+
)
|
|
75
|
+
unless @out.equal?($stdout) || preview_notice_emitted
|
|
76
|
+
@out.write(result.stdout)
|
|
77
|
+
end
|
|
78
|
+
@out.flush if @out.respond_to?(:flush)
|
|
79
|
+
@err.write(result.stderr) unless @err.equal?($stderr)
|
|
80
|
+
info("[cached]") if result.cached
|
|
81
|
+
result.exit_status
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve_app_module(name)
|
|
85
|
+
relative = name.tr(".", "/").sub(%r{^/}, "") + ".mt"
|
|
86
|
+
|
|
87
|
+
module_roots_for(Dir.pwd).each do |root|
|
|
88
|
+
candidate = File.join(root, "std", relative)
|
|
89
|
+
return File.expand_path(candidate) if File.file?(candidate)
|
|
90
|
+
|
|
91
|
+
candidate = File.join(root, relative)
|
|
92
|
+
return File.expand_path(candidate) if File.file?(candidate)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class CLI
|
|
5
|
+
module CommandSnapshot
|
|
6
|
+
def snapshot_command
|
|
7
|
+
input_path = nil
|
|
8
|
+
theme_path = nil
|
|
9
|
+
output_path = nil
|
|
10
|
+
textmate_only = false
|
|
11
|
+
|
|
12
|
+
until @argv.empty?
|
|
13
|
+
arg = @argv.first
|
|
14
|
+
case arg
|
|
15
|
+
when "--theme", "-t"
|
|
16
|
+
@argv.shift
|
|
17
|
+
theme_path = @argv.shift
|
|
18
|
+
unless theme_path
|
|
19
|
+
@err.puts("snapshot: missing value for --theme")
|
|
20
|
+
return 1
|
|
21
|
+
end
|
|
22
|
+
when "--output", "-o"
|
|
23
|
+
@argv.shift
|
|
24
|
+
output_path = @argv.shift
|
|
25
|
+
unless output_path
|
|
26
|
+
@err.puts("snapshot: missing value for --output")
|
|
27
|
+
return 1
|
|
28
|
+
end
|
|
29
|
+
when "--textmate-only"
|
|
30
|
+
@argv.shift
|
|
31
|
+
textmate_only = true
|
|
32
|
+
else
|
|
33
|
+
if arg.start_with?("-")
|
|
34
|
+
@err.puts("snapshot: unknown option #{arg}")
|
|
35
|
+
return 1
|
|
36
|
+
end
|
|
37
|
+
unless input_path
|
|
38
|
+
input_path = @argv.shift
|
|
39
|
+
else
|
|
40
|
+
@err.puts("snapshot: unexpected argument #{@argv.shift}")
|
|
41
|
+
return 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless input_path
|
|
47
|
+
@err.puts("snapshot: missing source file path")
|
|
48
|
+
print_usage(@err)
|
|
49
|
+
return 1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
unless File.file?(input_path)
|
|
53
|
+
@err.puts("snapshot: source file not found: #{input_path}")
|
|
54
|
+
return 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
input_path = File.expand_path(input_path)
|
|
58
|
+
theme_path = File.expand_path(theme_path) if theme_path
|
|
59
|
+
output_path = File.expand_path(output_path) if output_path
|
|
60
|
+
|
|
61
|
+
if theme_path && !File.file?(theme_path)
|
|
62
|
+
@err.puts("snapshot: theme file not found: #{theme_path}")
|
|
63
|
+
return 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
snapshot_script = MilkTea.root.join("bindings/vscode/scripts/snapshot.js").to_s
|
|
67
|
+
unless File.file?(snapshot_script)
|
|
68
|
+
@err.puts("snapshot: internal script not found at #{snapshot_script}")
|
|
69
|
+
return 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
args = ["node", snapshot_script, input_path]
|
|
73
|
+
args.push("-t", theme_path) if theme_path
|
|
74
|
+
args.push("-o", output_path) if output_path
|
|
75
|
+
|
|
76
|
+
semantic_result = nil
|
|
77
|
+
unless textmate_only
|
|
78
|
+
begin
|
|
79
|
+
semantic_result = MilkTea::LSP::Server.semantic_tokens_for_path(input_path)
|
|
80
|
+
rescue => e
|
|
81
|
+
@err.puts("snapshot: semantic analysis skipped: #{e.message}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if semantic_result && semantic_result[:entries] && !semantic_result[:entries].empty?
|
|
86
|
+
source = File.read(input_path)
|
|
87
|
+
lines = source.split("\n", -1)
|
|
88
|
+
semantic_result[:entries].each do |entry|
|
|
89
|
+
byte_start = entry[:startChar]
|
|
90
|
+
byte_len = entry[:length]
|
|
91
|
+
line_text = lines[entry[:line]]
|
|
92
|
+
unless line_text && byte_start && byte_len
|
|
93
|
+
entry[:startChar] = 0
|
|
94
|
+
entry[:length] = 0
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
char_start = line_text.byteslice(0, byte_start).length
|
|
98
|
+
char_length = line_text.byteslice(byte_start, byte_len).length
|
|
99
|
+
char_length = line_text.length - char_start if char_start + char_length > line_text.length
|
|
100
|
+
entry[:startChar] = char_start
|
|
101
|
+
entry[:length] = char_length
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
temp = Tempfile.new(["mt_semantic", ".json"])
|
|
105
|
+
temp.write(JSON.generate(semantic_result[:entries]))
|
|
106
|
+
temp.close
|
|
107
|
+
args.push("-s", temp.path)
|
|
108
|
+
system(*args)
|
|
109
|
+
temp.unlink
|
|
110
|
+
else
|
|
111
|
+
system(*args)
|
|
112
|
+
end
|
|
113
|
+
$?.success? ? 0 : 1
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|