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.
- 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/control_flow/builder.rb +2 -1
- 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 +11 -4
- data/lib/milk_tea/core/lowering/scans.rb +13 -18
- data/lib/milk_tea/core/module_binder.rb +19 -15
- data/lib/milk_tea/core/module_loader.rb +212 -59
- data/lib/milk_tea/core/module_path_resolver.rb +8 -7
- data/lib/milk_tea/core/parser/declarations.rb +43 -19
- data/lib/milk_tea/core/parser/expressions.rb +5 -8
- 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 +146 -92
- 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/core/types.rb +12 -17
- data/lib/milk_tea/lsp/diagnostics.rb +13 -0
- 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 +106 -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/sexpr_dumper.rb +7 -7
- metadata +24 -2
data/lib/milk_tea/tooling/cli.rb
CHANGED
|
@@ -146,1852 +146,130 @@ module MilkTea
|
|
|
146
146
|
1
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return invalid_color(value) unless valid_color?(value)
|
|
195
|
-
|
|
196
|
-
@color = value.to_sym
|
|
197
|
-
i += 1
|
|
198
|
-
else
|
|
199
|
-
remaining << arg
|
|
200
|
-
i += 1
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
@argv = remaining
|
|
204
|
-
true
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def valid_color?(value)
|
|
208
|
-
%w[auto always never].include?(value)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def invalid_color(value)
|
|
212
|
-
@err.puts("--color must be auto, always, or never#{value ? " (got #{value})" : ''}")
|
|
213
|
-
false
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def error_color?(io)
|
|
217
|
-
case @color
|
|
218
|
-
when :always then true
|
|
219
|
-
when :never then false
|
|
220
|
-
else io.respond_to?(:tty?) && io.tty?
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Prints an informational/progress line unless --quiet was given.
|
|
225
|
-
def info(message)
|
|
226
|
-
@out.puts(message) unless @quiet
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def lex_command
|
|
231
|
-
path = nil
|
|
232
|
-
sexpr = false
|
|
233
|
-
|
|
234
|
-
args = @argv.dup
|
|
235
|
-
@argv = []
|
|
236
|
-
until args.empty?
|
|
237
|
-
arg = args.shift
|
|
238
|
-
next if arg == "--"
|
|
239
|
-
|
|
240
|
-
if arg == "--sexpr"
|
|
241
|
-
sexpr = true
|
|
242
|
-
next
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
if path.nil?
|
|
246
|
-
path = arg
|
|
247
|
-
else
|
|
248
|
-
@err.puts("unknown option: #{arg}")
|
|
249
|
-
print_usage(@err)
|
|
250
|
-
return 1
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
unless path
|
|
255
|
-
@err.puts("missing source file path")
|
|
256
|
-
print_usage(@err)
|
|
257
|
-
return 1
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
tokens = Lexer.lex(read_source_file(path), path: path)
|
|
261
|
-
if sexpr
|
|
262
|
-
@out.puts(SexprDumper.dump_tokens(tokens))
|
|
263
|
-
else
|
|
264
|
-
@out.write(PP.pp(tokens, +""))
|
|
265
|
-
end
|
|
266
|
-
0
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def parse_command
|
|
270
|
-
sexpr = false
|
|
271
|
-
args = @argv.dup
|
|
272
|
-
@argv = []
|
|
273
|
-
until args.empty?
|
|
274
|
-
arg = args.shift
|
|
275
|
-
if arg == "--sexpr"
|
|
276
|
-
sexpr = true
|
|
277
|
-
next
|
|
278
|
-
end
|
|
279
|
-
@argv << arg
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
unless @argv.any?
|
|
283
|
-
@err.puts("missing source file path")
|
|
284
|
-
print_usage(@err)
|
|
285
|
-
return 1
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
resolution = extract_resolution_flags!
|
|
289
|
-
input_paths = @argv.dup
|
|
290
|
-
return 1 unless ensure_known_source_operands!("parse", input_paths)
|
|
291
|
-
|
|
292
|
-
paths = expand_source_paths(input_paths)
|
|
293
|
-
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
294
|
-
|
|
295
|
-
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
296
|
-
|
|
297
|
-
multiple = paths.length > 1
|
|
298
|
-
paths.each_with_index do |path, index|
|
|
299
|
-
ast = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).load_file(path)
|
|
300
|
-
if multiple
|
|
301
|
-
@out.puts("# --- #{path} ---") unless sexpr
|
|
302
|
-
end
|
|
303
|
-
if sexpr
|
|
304
|
-
@out.puts(SexprDumper.dump_ast(ast))
|
|
305
|
-
else
|
|
306
|
-
@out.write(PrettyPrinter.format_ast(ast))
|
|
307
|
-
end
|
|
308
|
-
@out.puts if multiple && index < paths.length - 1
|
|
309
|
-
end
|
|
310
|
-
0
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def format_command
|
|
314
|
-
parsed = parse_format_options
|
|
315
|
-
return 1 unless parsed
|
|
316
|
-
|
|
317
|
-
options = parsed[:options]
|
|
318
|
-
input_paths = parsed[:input_paths]
|
|
319
|
-
|
|
320
|
-
if input_paths.empty?
|
|
321
|
-
@err.puts("missing source file path")
|
|
322
|
-
print_usage(@err)
|
|
323
|
-
return 1
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
paths = expand_source_paths(input_paths)
|
|
327
|
-
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
328
|
-
|
|
329
|
-
multiple_sources = input_paths.length > 1 || input_paths.any? { |path| File.directory?(path) }
|
|
330
|
-
if multiple_sources
|
|
331
|
-
unless options[:check] || options[:write]
|
|
332
|
-
@err.puts("format on multiple sources requires --check or --write")
|
|
333
|
-
print_usage(@err)
|
|
334
|
-
return 1
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
return format_paths(paths, options)
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
path = paths.first
|
|
341
|
-
|
|
342
|
-
source = read_source_file(path)
|
|
343
|
-
format_profile = options[:profile] ? Linter::Profile.new : nil
|
|
344
|
-
start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
345
|
-
result = Formatter.check_source(source, path: path, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
|
|
346
|
-
elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
347
|
-
|
|
348
|
-
rc = if options[:check]
|
|
349
|
-
announce_file_action(path, "format-check")
|
|
350
|
-
if result.changed
|
|
351
|
-
info("needs formatting #{path}")
|
|
352
|
-
1
|
|
353
|
-
else
|
|
354
|
-
info("already formatted #{path}")
|
|
355
|
-
0
|
|
356
|
-
end
|
|
357
|
-
elsif options[:write]
|
|
358
|
-
announce_file_action(path, "format-write")
|
|
359
|
-
if result.changed
|
|
360
|
-
File.write(path, result.formatted_source)
|
|
361
|
-
info("formatted #{path}")
|
|
362
|
-
else
|
|
363
|
-
info("already formatted #{path}")
|
|
364
|
-
end
|
|
365
|
-
0
|
|
366
|
-
else
|
|
367
|
-
@out.write(result.formatted_source)
|
|
368
|
-
0
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
print_file_profiles([{ path:, total_ms: elapsed_ms, profile: format_profile }], "format") if options[:profile]
|
|
372
|
-
rc
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
def format_paths(paths, options)
|
|
376
|
-
format_profiles = []
|
|
377
|
-
if options[:check]
|
|
378
|
-
needs_fmt = []
|
|
379
|
-
paths.each do |p|
|
|
380
|
-
announce_file_action(p, "format-check")
|
|
381
|
-
format_profile = options[:profile] ? Linter::Profile.new : nil
|
|
382
|
-
start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
383
|
-
result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
|
|
384
|
-
elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
385
|
-
format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
|
|
386
|
-
needs_fmt << p if result.changed
|
|
387
|
-
end
|
|
388
|
-
print_file_profiles(format_profiles, "format") if options[:profile]
|
|
389
|
-
if needs_fmt.empty?
|
|
390
|
-
info("all #{paths.size} file(s) already formatted")
|
|
391
|
-
return 0
|
|
392
|
-
end
|
|
393
|
-
needs_fmt.each { |p| info("needs formatting #{p}") }
|
|
394
|
-
info("#{needs_fmt.size} file(s) need formatting")
|
|
395
|
-
return 1
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
# --write
|
|
399
|
-
changed = 0
|
|
400
|
-
paths.each do |p|
|
|
401
|
-
announce_file_action(p, "format-write")
|
|
402
|
-
format_profile = options[:profile] ? Linter::Profile.new : nil
|
|
403
|
-
start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
404
|
-
result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
|
|
405
|
-
elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
406
|
-
format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
|
|
407
|
-
if result.changed
|
|
408
|
-
File.write(p, result.formatted_source)
|
|
409
|
-
info("formatted #{p}")
|
|
410
|
-
changed += 1
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
print_file_profiles(format_profiles, "format") if options[:profile]
|
|
414
|
-
info("formatted #{changed} of #{paths.size} file(s)")
|
|
415
|
-
0
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def lint_command
|
|
419
|
-
resolution = { locked: false, frozen: false }
|
|
420
|
-
select = nil
|
|
421
|
-
ignore = nil
|
|
422
|
-
fix = false
|
|
423
|
-
init = false
|
|
424
|
-
ignore_generated = false
|
|
425
|
-
profile = false
|
|
426
|
-
input_paths = []
|
|
427
|
-
until @argv.empty?
|
|
428
|
-
arg = @argv.shift
|
|
429
|
-
unless arg.start_with?("--")
|
|
430
|
-
input_paths << arg
|
|
431
|
-
next
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
flag = arg
|
|
435
|
-
case flag
|
|
436
|
-
when "--select"
|
|
437
|
-
arg = @argv.shift
|
|
438
|
-
unless arg
|
|
439
|
-
@err.puts("--select requires a comma-separated list of rule codes")
|
|
440
|
-
return 1
|
|
441
|
-
end
|
|
442
|
-
select = arg.split(",").map(&:strip).to_set
|
|
443
|
-
when "--ignore"
|
|
444
|
-
arg = @argv.shift
|
|
445
|
-
unless arg
|
|
446
|
-
@err.puts("--ignore requires a comma-separated list of rule codes")
|
|
447
|
-
return 1
|
|
448
|
-
end
|
|
449
|
-
ignore = arg.split(",").map(&:strip).to_set
|
|
450
|
-
when "--fix"
|
|
451
|
-
fix = true
|
|
452
|
-
when "--init"
|
|
453
|
-
init = true
|
|
454
|
-
when "--locked"
|
|
455
|
-
resolution[:locked] = true
|
|
456
|
-
when "--frozen"
|
|
457
|
-
resolution[:locked] = true
|
|
458
|
-
resolution[:frozen] = true
|
|
459
|
-
when "--ignore-generated"
|
|
460
|
-
ignore_generated = true
|
|
461
|
-
when "--timings"
|
|
462
|
-
profile = true
|
|
463
|
-
when "--"
|
|
464
|
-
input_paths.concat(@argv)
|
|
465
|
-
@argv.clear
|
|
466
|
-
else
|
|
467
|
-
@err.puts("unknown lint flag: #{flag}")
|
|
468
|
-
return 1
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
if init
|
|
473
|
-
if input_paths.empty? && !select && !ignore && !fix && !resolution[:locked] && !resolution[:frozen]
|
|
474
|
-
return init_lint_config
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
@err.puts("--init does not accept source paths or lint options")
|
|
478
|
-
return 1
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
if input_paths.empty?
|
|
482
|
-
@err.puts("missing source file path")
|
|
483
|
-
print_usage(@err)
|
|
484
|
-
return 1
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
paths = input_paths.flat_map do |path|
|
|
488
|
-
if File.directory?(path)
|
|
489
|
-
Dir.glob(File.join(path, "**/*.mt")).sort
|
|
490
|
-
else
|
|
491
|
-
[path]
|
|
492
|
-
end
|
|
493
|
-
end.uniq
|
|
494
|
-
|
|
495
|
-
if paths.empty?
|
|
496
|
-
label = input_paths.length == 1 ? input_paths.first : input_paths.join(", ")
|
|
497
|
-
@out.puts("no .mt files found in #{label}")
|
|
498
|
-
return 0
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
502
|
-
|
|
503
|
-
if fix
|
|
504
|
-
lint_profiles = []
|
|
505
|
-
paths.each do |p|
|
|
506
|
-
announce_file_action(p, "lint-fix")
|
|
507
|
-
source = read_source_file(p)
|
|
508
|
-
if ignore_generated && generated_source?(source)
|
|
509
|
-
@out.puts("ignored generated #{p}")
|
|
510
|
-
next
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
facts = lint_sema_facts_for(source, p, locked: resolution[:locked])
|
|
514
|
-
prof = profile ? Linter::Profile.new : nil
|
|
515
|
-
start_time = profile ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
516
|
-
|
|
517
|
-
fixed = Linter.fix_source(
|
|
518
|
-
source,
|
|
519
|
-
path: p,
|
|
520
|
-
sema_facts: facts,
|
|
521
|
-
select:,
|
|
522
|
-
ignore:,
|
|
523
|
-
profile: prof,
|
|
524
|
-
)
|
|
525
|
-
total_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
526
|
-
lint_profiles << { path: p, profile: prof, mode: :pre_fix_scan, total_ms: } if prof
|
|
527
|
-
if fixed != source
|
|
528
|
-
File.write(p, fixed)
|
|
529
|
-
@out.puts("fixed #{p}")
|
|
530
|
-
end
|
|
531
|
-
end
|
|
532
|
-
print_lint_rule_profiles(lint_profiles) if profile
|
|
533
|
-
print_lint_file_profiles(lint_profiles) if profile
|
|
534
|
-
return 0
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
lint_profiles = []
|
|
538
|
-
all_warnings = paths.flat_map do |p|
|
|
539
|
-
announce_file_action(p, "lint")
|
|
540
|
-
source = read_source_file(p)
|
|
541
|
-
next [] if ignore_generated && generated_source?(source)
|
|
542
|
-
|
|
543
|
-
facts = lint_sema_facts_for(source, p, locked: resolution[:locked])
|
|
544
|
-
prof = profile ? Linter::Profile.new : nil
|
|
545
|
-
start_time = profile ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
546
|
-
warnings = Linter.lint_source(source, path: p, select:, ignore:, sema_facts: facts, profile: prof)
|
|
547
|
-
total_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
548
|
-
lint_profiles << { path: p, profile: prof, mode: :lint, total_ms: } if prof
|
|
549
|
-
warnings
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
if all_warnings.empty?
|
|
553
|
-
if input_paths.length == 1
|
|
554
|
-
info("clean #{input_paths.first}")
|
|
555
|
-
else
|
|
556
|
-
info("clean #{paths.size} file(s)")
|
|
557
|
-
end
|
|
558
|
-
print_lint_file_profiles(lint_profiles) if profile
|
|
559
|
-
return 0
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
all_warnings.each do |warning|
|
|
563
|
-
@out.puts("#{warning.path}:#{warning.line}: #{warning.code}: #{warning.message}")
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
print_lint_rule_profiles(lint_profiles) if profile
|
|
567
|
-
print_lint_file_profiles(lint_profiles) if profile
|
|
568
|
-
|
|
569
|
-
file_count = all_warnings.map(&:path).uniq.size
|
|
570
|
-
noun = all_warnings.size == 1 ? "warning" : "warnings"
|
|
571
|
-
files_str = file_count == 1 ? "1 file" : "#{file_count} files"
|
|
572
|
-
@out.puts("Found #{all_warnings.size} #{noun} in #{files_str}.")
|
|
573
|
-
1
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
def init_lint_config
|
|
577
|
-
path = File.join(Dir.pwd, Linter::DEFAULT_CONFIG_FILE_NAME)
|
|
578
|
-
if File.exist?(path)
|
|
579
|
-
@err.puts("lint config already exists at #{path}")
|
|
580
|
-
return 1
|
|
581
|
-
end
|
|
582
|
-
|
|
583
|
-
File.write(path, Linter.default_config_source)
|
|
584
|
-
info("created #{path}")
|
|
585
|
-
0
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
def print_lint_rule_profiles(lint_profiles, limit: 12)
|
|
589
|
-
lint_profiles.each do |entry|
|
|
590
|
-
profile = entry[:profile]
|
|
591
|
-
next unless profile
|
|
592
|
-
|
|
593
|
-
rows = profile.rule_breakdown(limit:, min_ms: 0.0)
|
|
594
|
-
rule_total = profile.total_time_ms(prefix: "rule.")
|
|
595
|
-
overall_total = profile.total_time_ms
|
|
596
|
-
mode_label = entry[:mode] == :pre_fix_scan ? "pre-fix scan" : "lint scan"
|
|
597
|
-
@out.puts("lint profile #{entry[:path]} (#{mode_label}): rules=#{format('%.1f', rule_total)}ms total=#{format('%.1f', overall_total)}ms")
|
|
598
|
-
|
|
599
|
-
if rows.empty?
|
|
600
|
-
@out.puts(" no rule timing data captured")
|
|
601
|
-
next
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
rows.each do |row|
|
|
605
|
-
share = rule_total.positive? ? ((row[:total_ms] / rule_total) * 100.0) : 0.0
|
|
606
|
-
@out.puts(
|
|
607
|
-
" #{row[:code]}: #{row[:count]}x total=#{format('%.1f', row[:total_ms])}ms avg=#{format('%.2f', row[:avg_ms])}ms share=#{format('%.1f', share)}%"
|
|
608
|
-
)
|
|
609
|
-
end
|
|
610
|
-
|
|
611
|
-
non_rule_rows = profile.timings_ms
|
|
612
|
-
.filter_map do |name, total_ms|
|
|
613
|
-
next if name.start_with?("rule.")
|
|
614
|
-
next if total_ms < 1.0
|
|
615
|
-
|
|
616
|
-
[name, total_ms]
|
|
617
|
-
end
|
|
618
|
-
.sort_by { |_name, total_ms| -total_ms }
|
|
619
|
-
.first(5)
|
|
620
|
-
.map do |name, total_ms|
|
|
621
|
-
count = profile.counts[name]
|
|
622
|
-
"#{name}:#{count}x/#{format('%.1f', total_ms)}ms"
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
@out.puts(" non-rule hot phases: #{non_rule_rows.join(', ')}") unless non_rule_rows.empty?
|
|
626
|
-
end
|
|
627
|
-
end
|
|
628
|
-
|
|
629
|
-
def print_lint_file_profiles(lint_profiles)
|
|
630
|
-
file_entries = lint_profiles.filter_map do |entry|
|
|
631
|
-
total = entry[:total_ms]
|
|
632
|
-
next unless total
|
|
633
|
-
|
|
634
|
-
phases = entry[:profile]&.timings_ms&.reject { |name, _| name.start_with?("rule.") }&.sort_by { |_, ms| -ms }
|
|
635
|
-
{ path: entry[:path], total_ms: total, phases: }
|
|
636
|
-
end
|
|
637
|
-
return if file_entries.empty?
|
|
638
|
-
|
|
639
|
-
sorted = file_entries.sort_by { |e| -e[:total_ms] }
|
|
640
|
-
@out.puts
|
|
641
|
-
@out.puts("Profile (lint): #{sorted.size} file(s)")
|
|
642
|
-
sorted.each do |entry|
|
|
643
|
-
phase_str = entry[:phases]&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 1.0 }&.join(", ")
|
|
644
|
-
detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
|
|
645
|
-
@out.puts(" #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
|
|
646
|
-
end
|
|
647
|
-
total = sorted.sum { |e| e[:total_ms] }
|
|
648
|
-
@out.puts("Total: #{format('%.1f', total)}ms")
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
def print_file_profiles(file_profiles, label)
|
|
652
|
-
sorted = file_profiles.sort_by { |fp| -fp[:total_ms] }
|
|
653
|
-
return if sorted.empty?
|
|
654
|
-
|
|
655
|
-
@out.puts
|
|
656
|
-
if sorted.size == 1
|
|
657
|
-
entry = sorted.first
|
|
658
|
-
phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
|
|
659
|
-
phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 0.1 }&.join(", ")
|
|
660
|
-
detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
|
|
661
|
-
@out.puts("#{label} profile #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
|
|
662
|
-
return
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
@out.puts("Profile (#{label}): #{sorted.size} file(s)")
|
|
666
|
-
sorted.each do |entry|
|
|
667
|
-
phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
|
|
668
|
-
phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 1.0 }&.join(", ")
|
|
669
|
-
detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
|
|
670
|
-
@out.puts(" #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
|
|
671
|
-
end
|
|
672
|
-
total = sorted.sum { |fp| fp[:total_ms] }
|
|
673
|
-
@out.puts("Total: #{format('%.1f', total)}ms")
|
|
674
|
-
end
|
|
675
|
-
|
|
676
|
-
def check_command
|
|
677
|
-
args = @argv.dup
|
|
678
|
-
@argv = []
|
|
679
|
-
until args.empty?
|
|
680
|
-
arg = args.shift
|
|
681
|
-
@argv << arg
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
unless @argv.any?
|
|
685
|
-
@err.puts("missing source file path")
|
|
686
|
-
print_usage(@err)
|
|
687
|
-
return 1
|
|
688
|
-
end
|
|
689
|
-
|
|
690
|
-
resolution = extract_resolution_flags!
|
|
691
|
-
input_paths = @argv.dup
|
|
692
|
-
return 1 unless ensure_known_source_operands!("check", input_paths)
|
|
693
|
-
|
|
694
|
-
paths = expand_source_paths(input_paths)
|
|
695
|
-
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
696
|
-
|
|
697
|
-
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
698
|
-
|
|
699
|
-
all_diagnostics = []
|
|
700
|
-
paths.each do |path|
|
|
701
|
-
diagnostics, module_name, closure_errors = check_single_reporting_all(path, locked: resolution[:locked])
|
|
702
|
-
closure_errors = [] if paths.length > 1
|
|
703
|
-
diagnostics = sort_by_location(diagnostics)
|
|
704
|
-
|
|
705
|
-
if diagnostics.any? || closure_errors.any?
|
|
706
|
-
main_source = read_source_file(path)
|
|
707
|
-
main_abs = File.expand_path(path)
|
|
708
|
-
diagnostics.each do |d|
|
|
709
|
-
same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
|
|
710
|
-
source = same_file ? main_source : nil
|
|
711
|
-
@err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
|
|
712
|
-
end
|
|
713
|
-
closure_errors.each do |d|
|
|
714
|
-
same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
|
|
715
|
-
source = same_file ? main_source : nil
|
|
716
|
-
@err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
|
|
717
|
-
end
|
|
718
|
-
all_diagnostics.concat(diagnostics)
|
|
719
|
-
all_diagnostics.concat(closure_errors)
|
|
720
|
-
elsif module_name
|
|
721
|
-
info("checked #{path} as #{module_name}")
|
|
722
|
-
end
|
|
723
|
-
end
|
|
724
|
-
|
|
725
|
-
return 0 if all_diagnostics.empty?
|
|
726
|
-
|
|
727
|
-
error_count = all_diagnostics.count { |d| !d.respond_to?(:severity) || d.severity == :error }
|
|
728
|
-
warning_count = all_diagnostics.count { |d| d.respond_to?(:severity) && d.severity == :warning }
|
|
729
|
-
info_count = all_diagnostics.count { |d| d.respond_to?(:severity) && (d.severity == :info || d.severity == :hint) }
|
|
730
|
-
|
|
731
|
-
@err.puts
|
|
732
|
-
parts = []
|
|
733
|
-
parts << "#{error_count} #{error_count == 1 ? 'error' : 'errors'}" if error_count > 0
|
|
734
|
-
parts << "#{warning_count} #{warning_count == 1 ? 'warning' : 'warnings'}" if warning_count > 0
|
|
735
|
-
parts << "#{info_count} #{info_count == 1 ? 'note' : 'notes'}" if info_count > 0
|
|
736
|
-
body = parts.join("; ")
|
|
737
|
-
if error_count > 0
|
|
738
|
-
@err.puts("#{body} found")
|
|
739
|
-
elsif warning_count > 0
|
|
740
|
-
@err.puts("#{body}")
|
|
741
|
-
end
|
|
742
|
-
final_error_count = error_count + (resolution[:warnings_as_errors] ? warning_count : 0)
|
|
743
|
-
final_error_count > 0 ? 1 : 0
|
|
744
|
-
end
|
|
745
|
-
|
|
746
|
-
def check_single_reporting_all(path, locked: false)
|
|
747
|
-
loader = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform)
|
|
748
|
-
resolved_path = File.expand_path(path)
|
|
749
|
-
ast = loader.load_file(resolved_path)
|
|
750
|
-
module_name = ast.module_name.to_s
|
|
751
|
-
|
|
752
|
-
import_result = loader.send(:imported_modules_for_ast_collecting_errors, ast, importer_path: resolved_path)
|
|
753
|
-
errors = import_result.errors.dup
|
|
754
|
-
|
|
755
|
-
analysis = nil
|
|
756
|
-
begin
|
|
757
|
-
result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path)
|
|
758
|
-
errors.concat(result[:errors])
|
|
759
|
-
analysis = result[:analysis]
|
|
760
|
-
rescue SemanticError => e
|
|
761
|
-
errors << e
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
if analysis && errors.empty?
|
|
765
|
-
source = read_source_file(path)
|
|
766
|
-
warnings = Linter.lint_source(source, path: resolved_path, sema_facts: analysis, lint_tier: :full)
|
|
767
|
-
errors.concat(warnings)
|
|
768
|
-
end
|
|
769
|
-
|
|
770
|
-
closure_errors = loader.collecting_path_errors.values.flatten.compact
|
|
771
|
-
[errors, module_name, closure_errors]
|
|
772
|
-
rescue ModuleLoadError, PackageLockError, SemanticError => e
|
|
773
|
-
[[e], nil, []]
|
|
774
|
-
end
|
|
775
|
-
|
|
776
|
-
def sort_by_location(errors)
|
|
777
|
-
errors.sort_by do |e|
|
|
778
|
-
actual = e.respond_to?(:error) ? e.error : e
|
|
779
|
-
line = actual.respond_to?(:line) ? actual.line.to_i : 0
|
|
780
|
-
column = actual.respond_to?(:column) ? actual.column.to_i : 0
|
|
781
|
-
[line, column]
|
|
782
|
-
end
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
def lower_command
|
|
786
|
-
sexpr = false
|
|
787
|
-
args = @argv.dup
|
|
788
|
-
@argv = []
|
|
789
|
-
until args.empty?
|
|
790
|
-
arg = args.shift
|
|
791
|
-
if arg == "--sexpr"
|
|
792
|
-
sexpr = true
|
|
793
|
-
next
|
|
794
|
-
end
|
|
795
|
-
@argv << arg
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
unless @argv.any?
|
|
799
|
-
@err.puts("missing source file path")
|
|
800
|
-
print_usage(@err)
|
|
801
|
-
return 1
|
|
802
|
-
end
|
|
803
|
-
|
|
804
|
-
resolution = extract_resolution_flags!
|
|
805
|
-
input_paths = @argv.dup
|
|
806
|
-
return 1 unless ensure_known_source_operands!("lower", input_paths)
|
|
807
|
-
|
|
808
|
-
paths = expand_source_paths(input_paths)
|
|
809
|
-
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
810
|
-
|
|
811
|
-
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
812
|
-
|
|
813
|
-
multiple = paths.length > 1
|
|
814
|
-
paths.each_with_index do |path, index|
|
|
815
|
-
program = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).check_program(path)
|
|
816
|
-
if multiple
|
|
817
|
-
@out.puts("# --- #{path} ---") unless sexpr
|
|
818
|
-
end
|
|
819
|
-
if sexpr
|
|
820
|
-
@out.puts(SexprDumper.dump_ir(Lowering.lower(program)))
|
|
821
|
-
else
|
|
822
|
-
@out.write(PrettyPrinter.format_ir(Lowering.lower(program)))
|
|
823
|
-
end
|
|
824
|
-
end
|
|
825
|
-
0
|
|
826
|
-
end
|
|
827
|
-
|
|
828
|
-
def emit_c_command
|
|
829
|
-
args = @argv.dup
|
|
830
|
-
@argv = []
|
|
831
|
-
until args.empty?
|
|
832
|
-
arg = args.shift
|
|
833
|
-
@argv << arg
|
|
834
|
-
end
|
|
835
|
-
|
|
836
|
-
unless @argv.any?
|
|
837
|
-
@err.puts("missing source file path")
|
|
838
|
-
print_usage(@err)
|
|
839
|
-
return 1
|
|
840
|
-
end
|
|
841
|
-
|
|
842
|
-
resolution = extract_resolution_flags!
|
|
843
|
-
input_paths = @argv.dup
|
|
844
|
-
return 1 unless ensure_known_source_operands!("emit-c", input_paths)
|
|
845
|
-
|
|
846
|
-
program_paths = input_paths.map { |p| resolve_program_path(p) }
|
|
847
|
-
return 1 if program_paths.include?(nil)
|
|
848
|
-
|
|
849
|
-
ensure_current_lockfiles!(input_paths) if resolution[:frozen]
|
|
850
|
-
|
|
851
|
-
multiple = program_paths.length > 1
|
|
852
|
-
program_paths.each_with_index do |path, index|
|
|
853
|
-
program = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).check_program(path)
|
|
854
|
-
if multiple
|
|
855
|
-
@out.puts("/* --- #{path} --- */")
|
|
856
|
-
end
|
|
857
|
-
@out.write(CBackend.generate_c(Lowering.lower(program), emit_line_directives: false))
|
|
858
|
-
@out.puts if multiple && index < program_paths.length - 1
|
|
859
|
-
end
|
|
860
|
-
0
|
|
861
|
-
end
|
|
862
|
-
|
|
863
|
-
def resolve_program_path(path)
|
|
864
|
-
return path unless File.directory?(path)
|
|
865
|
-
|
|
866
|
-
manifest_path = File.join(path, "package.toml")
|
|
867
|
-
unless File.file?(manifest_path)
|
|
868
|
-
@err.puts("no package.toml found in #{path}")
|
|
869
|
-
return nil
|
|
870
|
-
end
|
|
871
|
-
|
|
872
|
-
manifest = PackageManifest.load(path)
|
|
873
|
-
entry_path = manifest.source_path
|
|
874
|
-
unless entry_path && File.file?(entry_path)
|
|
875
|
-
@err.puts("no build entry found for #{path}")
|
|
876
|
-
return nil
|
|
877
|
-
end
|
|
878
|
-
|
|
879
|
-
entry_path
|
|
880
|
-
rescue PackageManifestError => e
|
|
881
|
-
@err.puts("failed to load package manifest for #{path}: #{e.message}")
|
|
882
|
-
nil
|
|
883
|
-
end
|
|
884
|
-
|
|
885
|
-
def build_command
|
|
886
|
-
path, options = extract_path_and_options(allow_clean: true)
|
|
887
|
-
return 1 unless path
|
|
888
|
-
|
|
889
|
-
if options.delete(:clean)
|
|
890
|
-
cleaned_path = Build.clean(path, output_path: options[:output_path], profile: options[:profile], platform: options[:platform], bundle: options[:bundle], archive: options[:archive])
|
|
891
|
-
info("cleaned #{cleaned_path}")
|
|
892
|
-
return 0
|
|
893
|
-
end
|
|
894
|
-
|
|
895
|
-
frozen = options.delete(:frozen)
|
|
896
|
-
ensure_current_lockfile!(path) if frozen
|
|
897
|
-
locked = options.delete(:locked)
|
|
898
|
-
bundle = options[:bundle]
|
|
899
|
-
package_graph = package_graph_for(path, locked:)
|
|
900
|
-
result = Build.build(path, module_roots: module_roots_for(path, locked:), package_graph:, frontend: @build_frontend, **options.except(:timings))
|
|
901
|
-
if bundle
|
|
902
|
-
info("built #{path} -> #{File.dirname(result.output_path)}")
|
|
903
|
-
info("entry executable #{result.output_path}")
|
|
904
|
-
info(" [cached]") if result.cached
|
|
905
|
-
info("archive #{result.archive_path}") if result.archive_path
|
|
906
|
-
elsif result.cached
|
|
907
|
-
info("built #{path} -> #{result.output_path} [cached]")
|
|
908
|
-
else
|
|
909
|
-
info("built #{path} -> #{result.output_path}")
|
|
910
|
-
end
|
|
911
|
-
info("saved C to #{result.c_path}") if result.c_path
|
|
912
|
-
0
|
|
913
|
-
end
|
|
914
|
-
|
|
915
|
-
def test_command
|
|
916
|
-
limits = extract_test_limit_flags!
|
|
917
|
-
return 1 unless limits
|
|
918
|
-
|
|
919
|
-
@test_timeout_seconds, @test_memory_bytes, @test_jobs, @test_sanitize, @test_filter, @test_format = limits
|
|
920
|
-
|
|
921
|
-
path, options = extract_path_and_options
|
|
922
|
-
return 1 unless path
|
|
923
|
-
|
|
924
|
-
frozen = options.delete(:frozen)
|
|
925
|
-
ensure_current_lockfile!(path) if frozen
|
|
926
|
-
locked = options.delete(:locked)
|
|
927
|
-
|
|
928
|
-
return dispatch_test_run(path, options:, locked:) if @test_format == :human
|
|
929
|
-
|
|
930
|
-
buffer = StringIO.new
|
|
931
|
-
real_out = @out
|
|
932
|
-
real_err = @err
|
|
933
|
-
@out = buffer
|
|
934
|
-
@err = buffer
|
|
935
|
-
begin
|
|
936
|
-
exit_code = dispatch_test_run(path, options:, locked:)
|
|
937
|
-
ensure
|
|
938
|
-
@out = real_out
|
|
939
|
-
@err = real_err
|
|
940
|
-
end
|
|
941
|
-
|
|
942
|
-
emit_machine_results(parse_test_output(buffer.string), @test_format, real_out)
|
|
943
|
-
exit_code
|
|
944
|
-
end
|
|
945
|
-
|
|
946
|
-
def dispatch_test_run(path, options:, locked:)
|
|
947
|
-
if File.directory?(path)
|
|
948
|
-
run_test_directory(path, options:, locked:)
|
|
949
|
-
elsif File.file?(path)
|
|
950
|
-
if compile_fail_fixture?(File.read(path))
|
|
951
|
-
run_compile_fail_test(path) ? 0 : 1
|
|
952
|
-
else
|
|
953
|
-
run_test_file(path, options:, locked:)
|
|
954
|
-
end
|
|
955
|
-
else
|
|
956
|
-
@err.puts("mtc test: not a file or directory: #{path}")
|
|
957
|
-
1
|
|
958
|
-
end
|
|
959
|
-
end
|
|
960
|
-
|
|
961
|
-
def extract_test_limit_flags!
|
|
962
|
-
timeout_seconds = TEST_RUN_TIMEOUT_SECONDS
|
|
963
|
-
memory_bytes = TEST_RUN_MEMORY_LIMIT_BYTES
|
|
964
|
-
jobs = 1
|
|
965
|
-
sanitize = false
|
|
966
|
-
filter = nil
|
|
967
|
-
format = :human
|
|
968
|
-
remaining = []
|
|
969
|
-
until @argv.empty?
|
|
970
|
-
arg = @argv.shift
|
|
971
|
-
case arg
|
|
972
|
-
when "--timeout"
|
|
973
|
-
value = @argv.shift
|
|
974
|
-
seconds = value && Integer(value, exception: false)
|
|
975
|
-
unless seconds&.positive?
|
|
976
|
-
@err.puts("--timeout requires a positive integer (seconds)")
|
|
977
|
-
return nil
|
|
978
|
-
end
|
|
979
|
-
timeout_seconds = seconds
|
|
980
|
-
when "--mem"
|
|
981
|
-
value = @argv.shift
|
|
982
|
-
megabytes = value && Integer(value, exception: false)
|
|
983
|
-
unless megabytes&.positive?
|
|
984
|
-
@err.puts("--mem requires a positive integer (megabytes)")
|
|
985
|
-
return nil
|
|
986
|
-
end
|
|
987
|
-
memory_bytes = megabytes * 1024 * 1024
|
|
988
|
-
when "--jobs"
|
|
989
|
-
value = @argv.shift
|
|
990
|
-
count = value && Integer(value, exception: false)
|
|
991
|
-
unless count&.positive?
|
|
992
|
-
@err.puts("--jobs requires a positive integer")
|
|
993
|
-
return nil
|
|
994
|
-
end
|
|
995
|
-
jobs = count
|
|
996
|
-
when "--sanitize"
|
|
997
|
-
sanitize = true
|
|
998
|
-
when "-n", "--name"
|
|
999
|
-
value = @argv.shift
|
|
1000
|
-
unless value
|
|
1001
|
-
@err.puts("-n requires a name substring")
|
|
1002
|
-
return nil
|
|
1003
|
-
end
|
|
1004
|
-
filter = value
|
|
1005
|
-
when "--format"
|
|
1006
|
-
value = @argv.shift
|
|
1007
|
-
unless %w[human tap junit].include?(value)
|
|
1008
|
-
@err.puts("--format must be human, tap, or junit")
|
|
1009
|
-
return nil
|
|
1010
|
-
end
|
|
1011
|
-
format = value.to_sym
|
|
1012
|
-
when "--"
|
|
1013
|
-
remaining << arg
|
|
1014
|
-
remaining.concat(@argv)
|
|
1015
|
-
@argv.clear
|
|
1016
|
-
else
|
|
1017
|
-
remaining << arg
|
|
1018
|
-
end
|
|
1019
|
-
end
|
|
1020
|
-
@argv.replace(remaining)
|
|
1021
|
-
[timeout_seconds, memory_bytes, jobs, sanitize, filter, format]
|
|
1022
|
-
end
|
|
1023
|
-
|
|
1024
|
-
TestResult = Data.define(:name, :status, :detail)
|
|
1025
|
-
|
|
1026
|
-
def parse_test_output(text)
|
|
1027
|
-
results = []
|
|
1028
|
-
current_file = nil
|
|
1029
|
-
text.each_line do |raw|
|
|
1030
|
-
line = raw.chomp
|
|
1031
|
-
case line
|
|
1032
|
-
when /\A# (.+)\z/
|
|
1033
|
-
current_file = ::Regexp.last_match(1)
|
|
1034
|
-
when /\Aok - (.+)\z/
|
|
1035
|
-
results << TestResult.new(name: ::Regexp.last_match(1), status: :pass, detail: nil)
|
|
1036
|
-
when /\Askip - (.+?)(?:: (.*))?\z/
|
|
1037
|
-
results << TestResult.new(name: ::Regexp.last_match(1), status: :skip, detail: ::Regexp.last_match(2))
|
|
1038
|
-
when /\AFAIL - (.+?)(?:: (.*))?\z/
|
|
1039
|
-
results << TestResult.new(name: ::Regexp.last_match(1), status: :fail, detail: ::Regexp.last_match(2))
|
|
1040
|
-
when /\AFAILED - (.+?) \(build error\)\z/
|
|
1041
|
-
results << TestResult.new(name: "#{::Regexp.last_match(1)} (build error)", status: :fail, detail: "build error")
|
|
1042
|
-
when /\Atest run (?:timed out|crashed)/, /\ASUMMARY: \w+Sanitizer/
|
|
1043
|
-
results << TestResult.new(name: "#{current_file || 'test'} (#{line})", status: :fail, detail: line)
|
|
1044
|
-
end
|
|
1045
|
-
end
|
|
1046
|
-
results
|
|
1047
|
-
end
|
|
1048
|
-
|
|
1049
|
-
def emit_machine_results(results, format, out)
|
|
1050
|
-
case format
|
|
1051
|
-
when :tap then emit_tap(results, out)
|
|
1052
|
-
when :junit then emit_junit(results, out)
|
|
1053
|
-
end
|
|
1054
|
-
end
|
|
1055
|
-
|
|
1056
|
-
def emit_tap(results, out)
|
|
1057
|
-
out.puts("TAP version 13")
|
|
1058
|
-
out.puts("1..#{results.length}")
|
|
1059
|
-
results.each_with_index do |result, index|
|
|
1060
|
-
number = index + 1
|
|
1061
|
-
case result.status
|
|
1062
|
-
when :pass
|
|
1063
|
-
out.puts("ok #{number} - #{result.name}")
|
|
1064
|
-
when :skip
|
|
1065
|
-
out.puts("ok #{number} - #{result.name} # SKIP#{result.detail ? " #{result.detail}" : ''}")
|
|
1066
|
-
when :fail
|
|
1067
|
-
out.puts("not ok #{number} - #{result.name}")
|
|
1068
|
-
next unless result.detail
|
|
1069
|
-
|
|
1070
|
-
out.puts(" ---")
|
|
1071
|
-
out.puts(" message: #{result.detail}")
|
|
1072
|
-
out.puts(" ...")
|
|
1073
|
-
end
|
|
1074
|
-
end
|
|
1075
|
-
end
|
|
1076
|
-
|
|
1077
|
-
def emit_junit(results, out)
|
|
1078
|
-
failures = results.count { |result| result.status == :fail }
|
|
1079
|
-
skipped = results.count { |result| result.status == :skip }
|
|
1080
|
-
out.puts(%(<?xml version="1.0" encoding="UTF-8"?>))
|
|
1081
|
-
out.puts(%(<testsuites tests="#{results.length}" failures="#{failures}" skipped="#{skipped}">))
|
|
1082
|
-
out.puts(%( <testsuite name="mtc test" tests="#{results.length}" failures="#{failures}" skipped="#{skipped}">))
|
|
1083
|
-
results.each do |result|
|
|
1084
|
-
name = xml_escape(result.name)
|
|
1085
|
-
case result.status
|
|
1086
|
-
when :pass
|
|
1087
|
-
out.puts(%( <testcase name="#{name}"/>))
|
|
1088
|
-
when :skip
|
|
1089
|
-
out.puts(%( <testcase name="#{name}"><skipped/></testcase>))
|
|
1090
|
-
when :fail
|
|
1091
|
-
out.puts(%( <testcase name="#{name}"><failure message="#{xml_escape(result.detail || 'failed')}"/></testcase>))
|
|
1092
|
-
end
|
|
1093
|
-
end
|
|
1094
|
-
out.puts(" </testsuite>")
|
|
1095
|
-
out.puts("</testsuites>")
|
|
1096
|
-
end
|
|
1097
|
-
|
|
1098
|
-
def xml_escape(value)
|
|
1099
|
-
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
1100
|
-
end
|
|
1101
|
-
|
|
1102
|
-
def run_test_directory(directory, options:, locked:)
|
|
1103
|
-
test_files = discover_test_files(directory)
|
|
1104
|
-
if test_files.empty?
|
|
1105
|
-
if @test_filter
|
|
1106
|
-
@out.puts("no tests matched -n '#{@test_filter}' under #{directory}")
|
|
1107
|
-
else
|
|
1108
|
-
@out.puts("no @[test] functions or # expect-error: fixtures found under #{directory}")
|
|
1109
|
-
end
|
|
1110
|
-
return 0
|
|
1111
|
-
end
|
|
1112
|
-
|
|
1113
|
-
jobs = @test_jobs || 1
|
|
1114
|
-
return run_test_files_parallel(test_files, jobs:, options:, locked:) if jobs > 1 && Process.respond_to?(:fork)
|
|
1115
|
-
|
|
1116
|
-
failed = 0
|
|
1117
|
-
test_files.each do |file, kind|
|
|
1118
|
-
@out.puts("# #{file}")
|
|
1119
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1120
|
-
failed += 1 unless run_classified_file(file, kind, options:, locked:).zero?
|
|
1121
|
-
end
|
|
1122
|
-
|
|
1123
|
-
@out.puts("")
|
|
1124
|
-
@out.puts("#{test_files.length} test file(s), #{failed} failed")
|
|
1125
|
-
failed.zero? ? 0 : 1
|
|
1126
|
-
end
|
|
1127
|
-
|
|
1128
|
-
def run_classified_file(file, kind, options:, locked:)
|
|
1129
|
-
if kind == :compile_fail
|
|
1130
|
-
run_compile_fail_test(file) ? 0 : 1
|
|
1131
|
-
else
|
|
1132
|
-
run_test_file_guarded(file, options:, locked:)
|
|
1133
|
-
end
|
|
1134
|
-
end
|
|
1135
|
-
|
|
1136
|
-
def run_compile_fail_test(path)
|
|
1137
|
-
source = File.read(path)
|
|
1138
|
-
expectations = extract_expect_error_directives(source)
|
|
1139
|
-
messages = compile_fail_diagnostics(path)
|
|
1140
|
-
|
|
1141
|
-
if messages.empty?
|
|
1142
|
-
@out.puts("FAIL - #{path} (compile-fail): expected a compile error, but it compiled cleanly")
|
|
1143
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1144
|
-
return false
|
|
1145
|
-
end
|
|
1146
|
-
|
|
1147
|
-
unmatched = expectations.find { |expected| messages.none? { |message| message.include?(expected) } }
|
|
1148
|
-
if unmatched
|
|
1149
|
-
@out.puts("FAIL - #{path} (compile-fail): no diagnostic matched #{unmatched.inspect}")
|
|
1150
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1151
|
-
return false
|
|
1152
|
-
end
|
|
1153
|
-
|
|
1154
|
-
@out.puts("ok - #{path} (compile-fail)")
|
|
1155
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1156
|
-
true
|
|
1157
|
-
end
|
|
1158
|
-
|
|
1159
|
-
def extract_expect_error_directives(source)
|
|
1160
|
-
source.each_line.filter_map do |line|
|
|
1161
|
-
match = line.match(/^\s*#\s*expect-error:\s*(.+?)\s*$/)
|
|
1162
|
-
match && match[1]
|
|
1163
|
-
end
|
|
1164
|
-
end
|
|
1165
|
-
|
|
1166
|
-
def compile_fail_diagnostics(path)
|
|
1167
|
-
errors, = check_single_reporting_all(path, locked: false)
|
|
1168
|
-
errors
|
|
1169
|
-
.select { |diagnostic| !diagnostic.respond_to?(:severity) || diagnostic.severity == :error }
|
|
1170
|
-
.map { |diagnostic| ErrorFormatter.format(diagnostic, color: false) }
|
|
1171
|
-
rescue StandardError => e
|
|
1172
|
-
raise unless handled_cli_error?(e)
|
|
1173
|
-
|
|
1174
|
-
[ErrorFormatter.format(e, color: false)]
|
|
1175
|
-
end
|
|
1176
|
-
|
|
1177
|
-
def run_test_file_guarded(file, options:, locked:)
|
|
1178
|
-
run_test_file(file, options:, locked:)
|
|
1179
|
-
rescue StandardError => e
|
|
1180
|
-
raise unless handled_cli_error?(e)
|
|
1181
|
-
|
|
1182
|
-
@err.puts("FAILED - #{file} (build error)")
|
|
1183
|
-
@err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
|
|
1184
|
-
1
|
|
1185
|
-
end
|
|
1186
|
-
|
|
1187
|
-
def run_test_files_parallel(test_files, jobs:, options:, locked:)
|
|
1188
|
-
results = Array.new(test_files.length)
|
|
1189
|
-
result_paths = {}
|
|
1190
|
-
active = {}
|
|
1191
|
-
cursor = 0
|
|
1192
|
-
|
|
1193
|
-
while cursor < test_files.length || !active.empty?
|
|
1194
|
-
while active.size < jobs && cursor < test_files.length
|
|
1195
|
-
index = cursor
|
|
1196
|
-
cursor += 1
|
|
1197
|
-
result_path = File.join(Dir.tmpdir, "mttest_result_#{Process.pid}_#{index}")
|
|
1198
|
-
result_paths[index] = result_path
|
|
1199
|
-
pid = fork do
|
|
1200
|
-
captured = StringIO.new
|
|
1201
|
-
@out = captured
|
|
1202
|
-
@err = captured
|
|
1203
|
-
file, kind = test_files[index]
|
|
1204
|
-
code = run_classified_file(file, kind, options:, locked:)
|
|
1205
|
-
File.binwrite(result_path, [code].pack("N") + captured.string)
|
|
1206
|
-
exit!(0)
|
|
1207
|
-
end
|
|
1208
|
-
active[pid] = index
|
|
1209
|
-
end
|
|
1210
|
-
|
|
1211
|
-
finished_pid, = Process.wait2
|
|
1212
|
-
index = active.delete(finished_pid)
|
|
1213
|
-
next unless index
|
|
1214
|
-
|
|
1215
|
-
path = result_paths[index]
|
|
1216
|
-
data = begin
|
|
1217
|
-
File.binread(path)
|
|
1218
|
-
rescue StandardError
|
|
1219
|
-
(+"").b
|
|
1220
|
-
end
|
|
1221
|
-
File.delete(path) if File.exist?(path)
|
|
1222
|
-
results[index] =
|
|
1223
|
-
if data.bytesize >= 4
|
|
1224
|
-
[data[0, 4].unpack1("N"), data.byteslice(4..).force_encoding(Encoding::UTF_8)]
|
|
1225
|
-
else
|
|
1226
|
-
[1, +""]
|
|
1227
|
-
end
|
|
1228
|
-
end
|
|
1229
|
-
|
|
1230
|
-
failed = 0
|
|
1231
|
-
test_files.each_with_index do |(file, _kind), index|
|
|
1232
|
-
code, output = results[index]
|
|
1233
|
-
@out.puts("# #{file}")
|
|
1234
|
-
@out.write(output.to_s)
|
|
1235
|
-
failed += 1 unless code&.zero?
|
|
1236
|
-
end
|
|
1237
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1238
|
-
@out.puts("")
|
|
1239
|
-
@out.puts("#{test_files.length} test file(s), #{failed} failed")
|
|
1240
|
-
failed.zero? ? 0 : 1
|
|
1241
|
-
end
|
|
1242
|
-
|
|
1243
|
-
def discover_test_files(directory)
|
|
1244
|
-
Dir.glob(File.join(directory, "**", "*.mt")).sort.filter_map do |file|
|
|
1245
|
-
next if File.basename(file).start_with?("__mt_test_runner_")
|
|
1246
|
-
|
|
1247
|
-
kind = classify_test_file(file)
|
|
1248
|
-
kind && [file, kind]
|
|
1249
|
-
end
|
|
1250
|
-
end
|
|
1251
|
-
|
|
1252
|
-
def classify_test_file(file)
|
|
1253
|
-
source = File.read(file)
|
|
1254
|
-
if compile_fail_fixture?(source)
|
|
1255
|
-
return nil if @test_filter && !File.basename(file, ".mt").include?(@test_filter)
|
|
1256
|
-
|
|
1257
|
-
return :compile_fail
|
|
1258
|
-
end
|
|
1259
|
-
|
|
1260
|
-
ast = begin
|
|
1261
|
-
MilkTea::Parser.parse(source, path: file)
|
|
1262
|
-
rescue ParseError
|
|
1263
|
-
return nil
|
|
1264
|
-
end
|
|
1265
|
-
has_match = ast.declarations.any? do |decl|
|
|
1266
|
-
decl.is_a?(AST::FunctionDef) && test_attribute?(decl) && matches_filter?(decl.name)
|
|
1267
|
-
end
|
|
1268
|
-
has_match ? :test : nil
|
|
1269
|
-
end
|
|
1270
|
-
|
|
1271
|
-
def compile_fail_fixture?(source)
|
|
1272
|
-
source.match?(/^\s*#\s*expect-error:/)
|
|
1273
|
-
end
|
|
1274
|
-
|
|
1275
|
-
def test_attribute?(decl)
|
|
1276
|
-
decl.attributes.any? { |attribute| attribute.name.parts == ["test"] }
|
|
1277
|
-
end
|
|
1278
|
-
|
|
1279
|
-
def matches_filter?(name)
|
|
1280
|
-
@test_filter.nil? || name.include?(@test_filter)
|
|
1281
|
-
end
|
|
1282
|
-
|
|
1283
|
-
def run_test_file(path, options:, locked:)
|
|
1284
|
-
source = File.read(path)
|
|
1285
|
-
ast = MilkTea::Parser.parse(source, path:)
|
|
1286
|
-
|
|
1287
|
-
if ast.declarations.any? { |decl| decl.is_a?(AST::FunctionDef) && decl.name == "main" }
|
|
1288
|
-
@err.puts("a test file must not define 'main': #{path}")
|
|
1289
|
-
return 1
|
|
1290
|
-
end
|
|
1291
|
-
|
|
1292
|
-
tests = ast.declarations.select { |decl| decl.is_a?(AST::FunctionDef) && test_attribute?(decl) }
|
|
1293
|
-
|
|
1294
|
-
if tests.empty?
|
|
1295
|
-
@out.puts("no @[test] functions found in #{path}")
|
|
1296
|
-
return 0
|
|
1297
|
-
end
|
|
1298
|
-
|
|
1299
|
-
tests = tests.select { |test| matches_filter?(test.name) }
|
|
1300
|
-
return 0 if tests.empty?
|
|
1301
|
-
|
|
1302
|
-
invalid = tests.find { |test| !test.params.empty? }
|
|
1303
|
-
if invalid
|
|
1304
|
-
@err.puts("@[test] function '#{invalid.name}' must take no parameters")
|
|
1305
|
-
return 1
|
|
1306
|
-
end
|
|
1307
|
-
|
|
1308
|
-
testing_import = ast.imports.find { |import| import.path.parts == %w[std testing] }
|
|
1309
|
-
unless testing_import
|
|
1310
|
-
@err.puts("a test file must import std.testing: #{path}")
|
|
1311
|
-
return 1
|
|
1312
|
-
end
|
|
1313
|
-
testing_alias = testing_import.alias_name || testing_import.path.parts.last
|
|
1314
|
-
|
|
1315
|
-
death_tests, normal_tests = tests.partition { |test| expect_fatal_attribute?(test) }
|
|
1316
|
-
|
|
1317
|
-
exit_code = 0
|
|
1318
|
-
|
|
1319
|
-
unless normal_tests.empty?
|
|
1320
|
-
runner_source = source.dup
|
|
1321
|
-
runner_source << "\n\n" << test_runner_main(testing_alias, normal_tests.map(&:name))
|
|
1322
|
-
exit_code = run_synthesized_tests(path, runner_source, options:, locked:)
|
|
1323
|
-
end
|
|
1324
|
-
|
|
1325
|
-
death_tests.each do |death_test|
|
|
1326
|
-
exit_code = 1 unless run_death_test(path, source, death_test.name, options:, locked:)
|
|
1327
|
-
end
|
|
1328
|
-
|
|
1329
|
-
exit_code
|
|
1330
|
-
end
|
|
1331
|
-
|
|
1332
|
-
def expect_fatal_attribute?(decl)
|
|
1333
|
-
decl.attributes.any? { |attribute| attribute.name.parts == ["expect_fatal"] }
|
|
1334
|
-
end
|
|
1335
|
-
|
|
1336
|
-
def run_death_test(source_path, source, test_name, options:, locked:)
|
|
1337
|
-
runner_source = source.dup
|
|
1338
|
-
runner_source << "\n\n" << death_test_runner_main(test_name)
|
|
1339
|
-
classification = with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
|
|
1340
|
-
classify_death_test(binary_path)
|
|
1341
|
-
end
|
|
1342
|
-
|
|
1343
|
-
passed = classification == :aborted
|
|
1344
|
-
line =
|
|
1345
|
-
if passed
|
|
1346
|
-
"ok - #{test_name} (expect_fatal)"
|
|
1347
|
-
elsif classification == :timed_out
|
|
1348
|
-
"FAIL - #{test_name} (expect_fatal): timed out"
|
|
1349
|
-
else
|
|
1350
|
-
"FAIL - #{test_name} (expect_fatal): expected a fatal abort, but the test returned"
|
|
1351
|
-
end
|
|
1352
|
-
@out.puts(line)
|
|
1353
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1354
|
-
passed
|
|
1355
|
-
end
|
|
1356
|
-
|
|
1357
|
-
def classify_death_test(binary_path)
|
|
1358
|
-
_output, status, timed_out = spawn_sandboxed(binary_path)
|
|
1359
|
-
return :timed_out if timed_out
|
|
1360
|
-
return :returned if status&.exited? && status.exitstatus&.zero?
|
|
1361
|
-
|
|
1362
|
-
:aborted
|
|
1363
|
-
end
|
|
1364
|
-
|
|
1365
|
-
def death_test_runner_main(test_name)
|
|
1366
|
-
[
|
|
1367
|
-
"function main() -> int:",
|
|
1368
|
-
" match #{test_name}():",
|
|
1369
|
-
" Result.success:",
|
|
1370
|
-
" return 0",
|
|
1371
|
-
" Result.failure:",
|
|
1372
|
-
" return 0",
|
|
1373
|
-
].join("\n") + "\n"
|
|
1374
|
-
end
|
|
1375
|
-
|
|
1376
|
-
def test_runner_main(testing_alias, test_names)
|
|
1377
|
-
lines = ["function main() -> int:"]
|
|
1378
|
-
lines << " var __mt_test_stats = #{testing_alias}.Stats.create()"
|
|
1379
|
-
test_names.each do |name|
|
|
1380
|
-
lines << " __mt_test_stats = #{testing_alias}.record(__mt_test_stats, #{name.inspect}, #{name}())"
|
|
1381
|
-
end
|
|
1382
|
-
lines << " return #{testing_alias}.summarize(__mt_test_stats)"
|
|
1383
|
-
lines.join("\n") + "\n"
|
|
1384
|
-
end
|
|
1385
|
-
|
|
1386
|
-
def run_synthesized_tests(source_path, runner_source, options:, locked:)
|
|
1387
|
-
with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
|
|
1388
|
-
run_test_binary(binary_path)
|
|
1389
|
-
end
|
|
1390
|
-
end
|
|
1391
|
-
|
|
1392
|
-
def with_synthesized_binary(source_path, runner_source, options:, locked:)
|
|
1393
|
-
directory = File.dirname(File.expand_path(source_path))
|
|
1394
|
-
runner_path = File.join(directory, "__mt_test_runner_#{Process.pid}.mt")
|
|
1395
|
-
binary_path = File.join(Dir.tmpdir, "__mt_test_runner_#{Process.pid}")
|
|
1396
|
-
|
|
1397
|
-
File.write(runner_path, runner_source)
|
|
1398
|
-
begin
|
|
1399
|
-
build_opts = options.except(:timings, :output_path, :bundle, :archive)
|
|
1400
|
-
build_opts[:debug_guards] = false
|
|
1401
|
-
Build.build(
|
|
1402
|
-
runner_path,
|
|
1403
|
-
output_path: binary_path,
|
|
1404
|
-
module_roots: module_roots_for(source_path, locked:),
|
|
1405
|
-
package_graph: package_graph_for(source_path, locked:),
|
|
1406
|
-
frontend: @build_frontend,
|
|
1407
|
-
sanitize: @test_sanitize,
|
|
1408
|
-
**build_opts,
|
|
1409
|
-
)
|
|
1410
|
-
yield binary_path
|
|
1411
|
-
ensure
|
|
1412
|
-
File.delete(runner_path) if File.exist?(runner_path)
|
|
1413
|
-
File.delete(binary_path) if File.exist?(binary_path)
|
|
1414
|
-
end
|
|
1415
|
-
end
|
|
1416
|
-
|
|
1417
|
-
TEST_RUN_TIMEOUT_SECONDS = 30
|
|
1418
|
-
TEST_RUN_MEMORY_LIMIT_BYTES = 1024 * 1024 * 1024
|
|
1419
|
-
|
|
1420
|
-
def run_test_binary(binary_path)
|
|
1421
|
-
output, status, timed_out = spawn_sandboxed(binary_path)
|
|
1422
|
-
@out.write(output)
|
|
1423
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1424
|
-
|
|
1425
|
-
if timed_out
|
|
1426
|
-
@err.puts("test run timed out after #{@test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS}s")
|
|
1427
|
-
return 1
|
|
1428
|
-
end
|
|
1429
|
-
if status&.signaled?
|
|
1430
|
-
@err.puts("test run crashed (signal #{status.termsig})")
|
|
1431
|
-
return 1
|
|
1432
|
-
end
|
|
1433
|
-
|
|
1434
|
-
status&.exitstatus || 1
|
|
1435
|
-
end
|
|
1436
|
-
|
|
1437
|
-
def spawn_sandboxed(binary_path)
|
|
1438
|
-
timeout_seconds = @test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS
|
|
1439
|
-
memory_bytes = @test_memory_bytes || TEST_RUN_MEMORY_LIMIT_BYTES
|
|
1440
|
-
reader, writer = IO.pipe
|
|
1441
|
-
spawn_options = { out: writer, err: writer, pgroup: true }
|
|
1442
|
-
spawn_options[:rlimit_as] = memory_bytes unless @test_sanitize
|
|
1443
|
-
pid = Process.spawn(binary_path, **spawn_options)
|
|
1444
|
-
writer.close
|
|
1445
|
-
|
|
1446
|
-
status = nil
|
|
1447
|
-
timed_out = false
|
|
1448
|
-
begin
|
|
1449
|
-
Timeout.timeout(timeout_seconds) { _, status = Process.wait2(pid) }
|
|
1450
|
-
rescue Timeout::Error
|
|
1451
|
-
timed_out = true
|
|
1452
|
-
begin
|
|
1453
|
-
Process.kill("-KILL", Process.getpgid(pid))
|
|
1454
|
-
Process.wait(pid)
|
|
1455
|
-
rescue StandardError
|
|
1456
|
-
nil
|
|
1457
|
-
end
|
|
1458
|
-
end
|
|
1459
|
-
|
|
1460
|
-
output = reader.read
|
|
1461
|
-
reader.close
|
|
1462
|
-
[output, status, timed_out]
|
|
1463
|
-
end
|
|
1464
|
-
|
|
1465
|
-
def run_command
|
|
1466
|
-
path, options = extract_path_and_options
|
|
1467
|
-
return 1 unless path
|
|
1468
|
-
|
|
1469
|
-
run_and_print_result(path, options)
|
|
1470
|
-
end
|
|
1471
|
-
|
|
1472
|
-
def app_command
|
|
1473
|
-
options = parse_build_options
|
|
1474
|
-
return 1 unless options
|
|
1475
|
-
|
|
1476
|
-
module_name = @argv.shift
|
|
1477
|
-
unless module_name
|
|
1478
|
-
@err.puts("missing module name")
|
|
1479
|
-
print_usage(@err)
|
|
1480
|
-
return 1
|
|
1481
|
-
end
|
|
1482
|
-
|
|
1483
|
-
path = resolve_app_module(module_name)
|
|
1484
|
-
unless path
|
|
1485
|
-
@err.puts("run-module module not found: #{module_name}")
|
|
1486
|
-
return 1
|
|
1487
|
-
end
|
|
1488
|
-
|
|
1489
|
-
frozen = options.delete(:frozen)
|
|
1490
|
-
ensure_current_lockfile!(path) if frozen
|
|
1491
|
-
|
|
1492
|
-
run_and_print_result(path, options)
|
|
1493
|
-
end
|
|
1494
|
-
|
|
1495
|
-
def extract_path_and_options(allow_clean: false)
|
|
1496
|
-
options = parse_build_options(allow_clean:)
|
|
1497
|
-
return nil unless options
|
|
1498
|
-
|
|
1499
|
-
path = @argv.shift
|
|
1500
|
-
unless path
|
|
1501
|
-
if File.file?(File.join(Dir.pwd, "package.toml"))
|
|
1502
|
-
path = Dir.pwd
|
|
1503
|
-
else
|
|
1504
|
-
@err.puts("missing source file path")
|
|
1505
|
-
print_usage(@err)
|
|
1506
|
-
return nil
|
|
1507
|
-
end
|
|
1508
|
-
end
|
|
1509
|
-
|
|
1510
|
-
[path, options]
|
|
1511
|
-
end
|
|
1512
|
-
|
|
1513
|
-
def run_and_print_result(path, options)
|
|
1514
|
-
frozen = options.delete(:frozen)
|
|
1515
|
-
ensure_current_lockfile!(path) if frozen
|
|
1516
|
-
locked = options.delete(:locked)
|
|
1517
|
-
package_graph = package_graph_for(path, locked:)
|
|
1518
|
-
preview_notice_emitted = false
|
|
1519
|
-
preview_started = lambda do |message|
|
|
1520
|
-
preview_notice_emitted = true
|
|
1521
|
-
@out.write(message)
|
|
1522
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1523
|
-
end
|
|
1524
|
-
|
|
1525
|
-
result = Run.run(
|
|
1526
|
-
path,
|
|
1527
|
-
module_roots: module_roots_for(path, locked:),
|
|
1528
|
-
package_graph:,
|
|
1529
|
-
frontend: @build_frontend,
|
|
1530
|
-
preview_started:,
|
|
1531
|
-
argv: @argv.dup,
|
|
1532
|
-
**options.except(:timings)
|
|
1533
|
-
)
|
|
1534
|
-
unless @out.equal?($stdout) || preview_notice_emitted
|
|
1535
|
-
@out.write(result.stdout)
|
|
1536
|
-
end
|
|
1537
|
-
@out.flush if @out.respond_to?(:flush)
|
|
1538
|
-
@err.write(result.stderr) unless @err.equal?($stderr)
|
|
1539
|
-
info("[cached]") if result.cached
|
|
1540
|
-
result.exit_status
|
|
1541
|
-
end
|
|
1542
|
-
|
|
1543
|
-
def resolve_app_module(name)
|
|
1544
|
-
relative = name.tr(".", "/").sub(%r{^/}, "") + ".mt"
|
|
1545
|
-
|
|
1546
|
-
module_roots_for(Dir.pwd).each do |root|
|
|
1547
|
-
candidate = File.join(root, "std", relative)
|
|
1548
|
-
return File.expand_path(candidate) if File.file?(candidate)
|
|
1549
|
-
|
|
1550
|
-
candidate = File.join(root, relative)
|
|
1551
|
-
return File.expand_path(candidate) if File.file?(candidate)
|
|
1552
|
-
end
|
|
1553
|
-
|
|
1554
|
-
nil
|
|
1555
|
-
end
|
|
149
|
+
require_relative "cli/commands/command_base"
|
|
150
|
+
require_relative "cli/commands/lex"
|
|
151
|
+
require_relative "cli/commands/parse"
|
|
152
|
+
require_relative "cli/commands/format"
|
|
153
|
+
require_relative "cli/commands/lint"
|
|
154
|
+
require_relative "cli/commands/check"
|
|
155
|
+
require_relative "cli/commands/lower"
|
|
156
|
+
require_relative "cli/commands/emit_c"
|
|
157
|
+
require_relative "cli/commands/build"
|
|
158
|
+
require_relative "cli/commands/test"
|
|
159
|
+
require_relative "cli/commands/run"
|
|
160
|
+
require_relative "cli/commands/new"
|
|
161
|
+
require_relative "cli/commands/debug"
|
|
162
|
+
require_relative "cli/commands/toolchain"
|
|
163
|
+
require_relative "cli/commands/deps"
|
|
164
|
+
require_relative "cli/commands/bindgen"
|
|
165
|
+
require_relative "cli/commands/cache"
|
|
166
|
+
require_relative "cli/commands/docs"
|
|
167
|
+
require_relative "cli/commands/snapshot"
|
|
168
|
+
require_relative "cli/commands/lsp"
|
|
169
|
+
require_relative "cli/commands/dap"
|
|
170
|
+
require_relative "cli/commands/completions"
|
|
171
|
+
|
|
172
|
+
include CommandBase
|
|
173
|
+
include CommandLex
|
|
174
|
+
include CommandParse
|
|
175
|
+
include CommandFormat
|
|
176
|
+
include CommandLint
|
|
177
|
+
include CommandCheck
|
|
178
|
+
include CommandLower
|
|
179
|
+
include CommandEmitC
|
|
180
|
+
include CommandBuild
|
|
181
|
+
include CommandTest
|
|
182
|
+
include CommandRun
|
|
183
|
+
include CommandNew
|
|
184
|
+
include CommandDebug
|
|
185
|
+
include CommandToolchain
|
|
186
|
+
include CommandDeps
|
|
187
|
+
include CommandBindgen
|
|
188
|
+
include CommandCache
|
|
189
|
+
include CommandDocs
|
|
190
|
+
include CommandSnapshot
|
|
191
|
+
include CommandLsp
|
|
192
|
+
include CommandDap
|
|
193
|
+
include CommandCompletions
|
|
1556
194
|
|
|
1557
|
-
|
|
1558
|
-
name = @argv.shift
|
|
1559
|
-
unless name
|
|
1560
|
-
@err.puts("missing project name")
|
|
1561
|
-
print_usage(@err)
|
|
1562
|
-
return 1
|
|
1563
|
-
end
|
|
1564
|
-
|
|
1565
|
-
if @argv.any?
|
|
1566
|
-
@err.puts("unknown new option #{@argv.first}")
|
|
1567
|
-
print_usage(@err)
|
|
1568
|
-
return 1
|
|
1569
|
-
end
|
|
1570
|
-
|
|
1571
|
-
result = ProjectScaffold.create(name)
|
|
1572
|
-
info("created #{result.root_path}")
|
|
1573
|
-
0
|
|
1574
|
-
end
|
|
1575
|
-
|
|
1576
|
-
def deps_command
|
|
1577
|
-
PackageManagerCLI.start(
|
|
1578
|
-
@argv,
|
|
1579
|
-
out: @out,
|
|
1580
|
-
err: @err,
|
|
1581
|
-
help_printer: method(:print_deps_help),
|
|
1582
|
-
services: package_services,
|
|
1583
|
-
)
|
|
1584
|
-
end
|
|
1585
|
-
|
|
1586
|
-
def docs_command
|
|
1587
|
-
port = nil
|
|
1588
|
-
open_flag = false
|
|
1589
|
-
|
|
1590
|
-
while (arg = @argv.first)
|
|
1591
|
-
case arg
|
|
1592
|
-
when "--port", "-p"
|
|
1593
|
-
@argv.shift
|
|
1594
|
-
port = @argv.shift.to_i
|
|
1595
|
-
port = nil if port <= 0 || port > 65535
|
|
1596
|
-
when "--open", "-o"
|
|
1597
|
-
open_flag = true
|
|
1598
|
-
@argv.shift
|
|
1599
|
-
else
|
|
1600
|
-
break
|
|
1601
|
-
end
|
|
1602
|
-
end
|
|
1603
|
-
|
|
1604
|
-
port = resolve_docs_port(port)
|
|
1605
|
-
|
|
1606
|
-
DocsApp.set :port, port
|
|
1607
|
-
DocsApp.set :bind, "127.0.0.1"
|
|
1608
|
-
DocsApp.set :environment, :production
|
|
1609
|
-
DocsApp.set :server, :puma
|
|
1610
|
-
|
|
1611
|
-
url = "http://127.0.0.1:#{port}/"
|
|
1612
|
-
|
|
1613
|
-
@out.puts("Serving Milk Tea docs at #{url}")
|
|
1614
|
-
@out.puts("Press Ctrl+C to stop.")
|
|
1615
|
-
|
|
1616
|
-
if open_flag
|
|
1617
|
-
open_browser(url)
|
|
1618
|
-
end
|
|
195
|
+
private
|
|
1619
196
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
rescue Interrupt
|
|
1623
|
-
0
|
|
197
|
+
def version_request?(command)
|
|
198
|
+
%w[version --version -V].include?(command)
|
|
1624
199
|
end
|
|
1625
200
|
|
|
1626
|
-
def
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
server = TCPServer.new("127.0.0.1", 0)
|
|
1630
|
-
port = server.addr[1]
|
|
1631
|
-
server.close
|
|
1632
|
-
port
|
|
1633
|
-
rescue StandardError
|
|
1634
|
-
4567
|
|
201
|
+
def help_request?(command)
|
|
202
|
+
command.nil? || %w[help --help -h].include?(command)
|
|
1635
203
|
end
|
|
1636
204
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
return 1
|
|
1652
|
-
end
|
|
1653
|
-
when "--output", "-o"
|
|
1654
|
-
@argv.shift
|
|
1655
|
-
output_path = @argv.shift
|
|
1656
|
-
unless output_path
|
|
1657
|
-
@err.puts("snapshot: missing value for --output")
|
|
1658
|
-
return 1
|
|
1659
|
-
end
|
|
1660
|
-
when "--textmate-only"
|
|
1661
|
-
@argv.shift
|
|
1662
|
-
textmate_only = true
|
|
1663
|
-
else
|
|
1664
|
-
if arg.start_with?("-")
|
|
1665
|
-
@err.puts("snapshot: unknown option #{arg}")
|
|
1666
|
-
return 1
|
|
1667
|
-
end
|
|
1668
|
-
unless input_path
|
|
1669
|
-
input_path = @argv.shift
|
|
1670
|
-
else
|
|
1671
|
-
@err.puts("snapshot: unexpected argument #{@argv.shift}")
|
|
1672
|
-
return 1
|
|
1673
|
-
end
|
|
1674
|
-
end
|
|
1675
|
-
end
|
|
1676
|
-
|
|
1677
|
-
unless input_path
|
|
1678
|
-
@err.puts("snapshot: missing source file path")
|
|
1679
|
-
print_usage(@err)
|
|
1680
|
-
return 1
|
|
1681
|
-
end
|
|
1682
|
-
|
|
1683
|
-
unless File.file?(input_path)
|
|
1684
|
-
@err.puts("snapshot: source file not found: #{input_path}")
|
|
1685
|
-
return 1
|
|
1686
|
-
end
|
|
1687
|
-
|
|
1688
|
-
input_path = File.expand_path(input_path)
|
|
1689
|
-
theme_path = File.expand_path(theme_path) if theme_path
|
|
1690
|
-
output_path = File.expand_path(output_path) if output_path
|
|
1691
|
-
|
|
1692
|
-
if theme_path && !File.file?(theme_path)
|
|
1693
|
-
@err.puts("snapshot: theme file not found: #{theme_path}")
|
|
1694
|
-
return 1
|
|
1695
|
-
end
|
|
1696
|
-
|
|
1697
|
-
snapshot_script = MilkTea.root.join("bindings/vscode/scripts/snapshot.js").to_s
|
|
1698
|
-
unless File.file?(snapshot_script)
|
|
1699
|
-
@err.puts("snapshot: internal script not found at #{snapshot_script}")
|
|
1700
|
-
return 1
|
|
1701
|
-
end
|
|
1702
|
-
|
|
1703
|
-
args = ["node", snapshot_script, input_path]
|
|
1704
|
-
args.push("-t", theme_path) if theme_path
|
|
1705
|
-
args.push("-o", output_path) if output_path
|
|
1706
|
-
|
|
1707
|
-
semantic_result = nil
|
|
1708
|
-
unless textmate_only
|
|
1709
|
-
begin
|
|
1710
|
-
semantic_result = MilkTea::LSP::Server.semantic_tokens_for_path(input_path)
|
|
1711
|
-
rescue => e
|
|
1712
|
-
@err.puts("snapshot: semantic analysis skipped: #{e.message}")
|
|
1713
|
-
end
|
|
1714
|
-
end
|
|
1715
|
-
|
|
1716
|
-
if semantic_result && semantic_result[:entries] && !semantic_result[:entries].empty?
|
|
1717
|
-
source = File.read(input_path)
|
|
1718
|
-
lines = source.split("\n", -1)
|
|
1719
|
-
semantic_result[:entries].each do |entry|
|
|
1720
|
-
byte_start = entry[:startChar]
|
|
1721
|
-
byte_len = entry[:length]
|
|
1722
|
-
line_text = lines[entry[:line]]
|
|
1723
|
-
unless line_text && byte_start && byte_len
|
|
1724
|
-
entry[:startChar] = 0
|
|
1725
|
-
entry[:length] = 0
|
|
1726
|
-
next
|
|
1727
|
-
end
|
|
1728
|
-
char_start = line_text.byteslice(0, byte_start).length
|
|
1729
|
-
char_length = line_text.byteslice(byte_start, byte_len).length
|
|
1730
|
-
char_length = line_text.length - char_start if char_start + char_length > line_text.length
|
|
1731
|
-
entry[:startChar] = char_start
|
|
1732
|
-
entry[:length] = char_length
|
|
205
|
+
# Pulls global options that may appear before the first `--` separator out of
|
|
206
|
+
# @argv so each subcommand parser sees a clean argument list. Stops at `--` so
|
|
207
|
+
# arguments forwarded to a run target (e.g. `mtc run app -- --verbose`) are
|
|
208
|
+
# preserved verbatim.
|
|
209
|
+
def extract_global_options!
|
|
210
|
+
remaining = []
|
|
211
|
+
forwarding = false
|
|
212
|
+
i = 0
|
|
213
|
+
while i < @argv.length
|
|
214
|
+
arg = @argv[i]
|
|
215
|
+
if forwarding
|
|
216
|
+
remaining << arg
|
|
217
|
+
i += 1
|
|
218
|
+
next
|
|
1733
219
|
end
|
|
1734
220
|
|
|
1735
|
-
temp = Tempfile.new(["mt_semantic", ".json"])
|
|
1736
|
-
temp.write(JSON.generate(semantic_result[:entries]))
|
|
1737
|
-
temp.close
|
|
1738
|
-
args.push("-s", temp.path)
|
|
1739
|
-
system(*args)
|
|
1740
|
-
temp.unlink
|
|
1741
|
-
else
|
|
1742
|
-
system(*args)
|
|
1743
|
-
end
|
|
1744
|
-
$?.success? ? 0 : 1
|
|
1745
|
-
end
|
|
1746
|
-
|
|
1747
|
-
def lsp_command
|
|
1748
|
-
log_level = nil
|
|
1749
|
-
|
|
1750
|
-
until @argv.empty?
|
|
1751
|
-
arg = @argv.shift
|
|
1752
221
|
case arg
|
|
1753
|
-
when "--
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
# stdio is the only transport; accept the flag as a no-op
|
|
1767
|
-
else
|
|
1768
|
-
if arg.start_with?("-")
|
|
1769
|
-
@err.puts("lsp: unknown option #{arg}")
|
|
1770
|
-
return 1
|
|
1771
|
-
end
|
|
1772
|
-
@err.puts("lsp: unexpected argument #{arg}")
|
|
1773
|
-
return 1
|
|
1774
|
-
end
|
|
1775
|
-
end
|
|
1776
|
-
|
|
1777
|
-
require "milk_tea/lsp/server" unless defined?(MilkTea::LSP::Server)
|
|
1778
|
-
server = MilkTea::LSP::Server.new
|
|
1779
|
-
server.run
|
|
1780
|
-
0
|
|
1781
|
-
end
|
|
222
|
+
when "--"
|
|
223
|
+
forwarding = true
|
|
224
|
+
remaining << arg
|
|
225
|
+
i += 1
|
|
226
|
+
when "-v", "--verbose"
|
|
227
|
+
@verbose = true
|
|
228
|
+
i += 1
|
|
229
|
+
when "-q", "--quiet"
|
|
230
|
+
@quiet = true
|
|
231
|
+
i += 1
|
|
232
|
+
when "--color"
|
|
233
|
+
value = @argv[i + 1]
|
|
234
|
+
return invalid_color(value) unless valid_color?(value)
|
|
1782
235
|
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
236
|
+
@color = value.to_sym
|
|
237
|
+
i += 2
|
|
238
|
+
when /\A--color=(.*)\z/
|
|
239
|
+
value = ::Regexp.last_match(1)
|
|
240
|
+
return invalid_color(value) unless valid_color?(value)
|
|
1786
241
|
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
case arg
|
|
1790
|
-
when "--log-level"
|
|
1791
|
-
@argv.shift
|
|
1792
|
-
when /\A--log-level=(.+)\z/
|
|
1793
|
-
# accept and ignore
|
|
1794
|
-
when "--backend"
|
|
1795
|
-
preferred_backend_kind = @argv.shift&.downcase
|
|
1796
|
-
when /\A--backend=(.+)\z/
|
|
1797
|
-
preferred_backend_kind = ::Regexp.last_match(1).downcase
|
|
1798
|
-
when "--adapter-path"
|
|
1799
|
-
adapter_path = @argv.shift
|
|
1800
|
-
adapter_command = resolve_adapter_path(adapter_path)
|
|
1801
|
-
return 1 unless adapter_command
|
|
1802
|
-
when /\A--adapter-path=(.+)\z/
|
|
1803
|
-
adapter_path = ::Regexp.last_match(1)
|
|
1804
|
-
adapter_command = resolve_adapter_path(adapter_path)
|
|
1805
|
-
return 1 unless adapter_command
|
|
242
|
+
@color = value.to_sym
|
|
243
|
+
i += 1
|
|
1806
244
|
else
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
return 1
|
|
1810
|
-
end
|
|
1811
|
-
@err.puts("dap: unexpected argument #{arg}")
|
|
1812
|
-
return 1
|
|
1813
|
-
end
|
|
1814
|
-
end
|
|
1815
|
-
|
|
1816
|
-
require "milk_tea/dap/server" unless defined?(MilkTea::DAP::Server)
|
|
1817
|
-
server = MilkTea::DAP::Server.new(
|
|
1818
|
-
preferred_backend_kind:,
|
|
1819
|
-
adapter_command:,
|
|
1820
|
-
)
|
|
1821
|
-
server.run
|
|
1822
|
-
0
|
|
1823
|
-
end
|
|
1824
|
-
|
|
1825
|
-
def resolve_adapter_path(adapter_path)
|
|
1826
|
-
unless adapter_path && File.file?(adapter_path)
|
|
1827
|
-
@err.puts("dap: adapter path not found: #{adapter_path}")
|
|
1828
|
-
return nil
|
|
1829
|
-
end
|
|
1830
|
-
expanded = File.expand_path(adapter_path)
|
|
1831
|
-
adapter_path.end_with?(".rb") ? [RbConfig.ruby, expanded] : [expanded]
|
|
1832
|
-
end
|
|
1833
|
-
|
|
1834
|
-
def toolchain_command
|
|
1835
|
-
ToolchainCLI.start(
|
|
1836
|
-
@argv,
|
|
1837
|
-
out: @out,
|
|
1838
|
-
err: @err,
|
|
1839
|
-
help_printer: method(:print_toolchain_help),
|
|
1840
|
-
)
|
|
1841
|
-
end
|
|
1842
|
-
|
|
1843
|
-
def debug_command
|
|
1844
|
-
unless @argv.any?
|
|
1845
|
-
@err.puts("missing source file path")
|
|
1846
|
-
print_usage(@err)
|
|
1847
|
-
return 1
|
|
1848
|
-
end
|
|
1849
|
-
|
|
1850
|
-
resolution = extract_resolution_flags!
|
|
1851
|
-
input_paths = @argv.dup
|
|
1852
|
-
return 1 unless ensure_known_source_operands!("debug", input_paths)
|
|
1853
|
-
|
|
1854
|
-
path = expand_source_paths(input_paths).first
|
|
1855
|
-
unless path
|
|
1856
|
-
@err.puts("no .mt files found in #{input_paths.join(', ')}")
|
|
1857
|
-
return 1
|
|
1858
|
-
end
|
|
1859
|
-
|
|
1860
|
-
ensure_current_lockfile!(path) if resolution[:frozen]
|
|
1861
|
-
|
|
1862
|
-
source = read_source_file(path)
|
|
1863
|
-
resolved_path = File.expand_path(path)
|
|
1864
|
-
|
|
1865
|
-
tokens = MilkTea::Lexer.lex(source, path: resolved_path)
|
|
1866
|
-
|
|
1867
|
-
parse_result = MilkTea::Parser.parse_collecting_errors(source, path: resolved_path)
|
|
1868
|
-
ast = parse_result.ast
|
|
1869
|
-
parse_errors = parse_result.errors.dup
|
|
1870
|
-
|
|
1871
|
-
facts = nil
|
|
1872
|
-
snapshot = nil
|
|
1873
|
-
loader_ast = ast
|
|
1874
|
-
|
|
1875
|
-
if ast && parse_errors.empty?
|
|
1876
|
-
begin
|
|
1877
|
-
loader = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform)
|
|
1878
|
-
loader_ast = loader.load_file(resolved_path)
|
|
1879
|
-
|
|
1880
|
-
import_result = loader.send(:imported_modules_for_ast_collecting_errors, loader_ast, importer_path: resolved_path)
|
|
1881
|
-
import_errors = import_result.respond_to?(:errors) ? import_result.errors : []
|
|
1882
|
-
parse_errors.concat(import_errors) unless import_errors.empty?
|
|
1883
|
-
|
|
1884
|
-
snapshot = MilkTea::SemanticAnalyzer.tooling_snapshot(
|
|
1885
|
-
loader_ast,
|
|
1886
|
-
imported_modules: import_result.modules,
|
|
1887
|
-
allow_missing_imports: true,
|
|
1888
|
-
path: resolved_path,
|
|
1889
|
-
)
|
|
1890
|
-
facts = snapshot&.facts
|
|
1891
|
-
rescue MilkTea::LexError, MilkTea::ParseError, ModuleLoadError, SemanticError => e
|
|
1892
|
-
parse_errors << e
|
|
245
|
+
remaining << arg
|
|
246
|
+
i += 1
|
|
1893
247
|
end
|
|
1894
248
|
end
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
content: source,
|
|
1898
|
-
tokens: tokens,
|
|
1899
|
-
ast: loader_ast,
|
|
1900
|
-
parse_errors: parse_errors,
|
|
1901
|
-
facts: facts,
|
|
1902
|
-
snapshot: snapshot,
|
|
1903
|
-
path: resolved_path,
|
|
1904
|
-
)
|
|
1905
|
-
|
|
1906
|
-
@out.puts(text)
|
|
1907
|
-
0
|
|
1908
|
-
rescue MilkTea::LexError => e
|
|
1909
|
-
@err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
|
|
1910
|
-
1
|
|
249
|
+
@argv = remaining
|
|
250
|
+
true
|
|
1911
251
|
end
|
|
1912
252
|
|
|
1913
|
-
def
|
|
1914
|
-
|
|
253
|
+
def valid_color?(value)
|
|
254
|
+
%w[auto always never].include?(value)
|
|
1915
255
|
end
|
|
1916
256
|
|
|
1917
|
-
def
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
@err.puts("missing cache subcommand")
|
|
1921
|
-
print_command_help("cache", @err)
|
|
1922
|
-
return 1
|
|
1923
|
-
end
|
|
1924
|
-
|
|
1925
|
-
cache_root = MilkTea.data_root.join("tmp", "mtc-cache")
|
|
1926
|
-
|
|
1927
|
-
case subcommand
|
|
1928
|
-
when "purge"
|
|
1929
|
-
if File.directory?(cache_root)
|
|
1930
|
-
FileUtils.rm_rf(cache_root)
|
|
1931
|
-
@out.puts("purged #{cache_root}")
|
|
1932
|
-
else
|
|
1933
|
-
@out.puts("cache is already empty")
|
|
1934
|
-
end
|
|
1935
|
-
0
|
|
1936
|
-
when "status"
|
|
1937
|
-
unless File.directory?(cache_root)
|
|
1938
|
-
@out.puts("cache directory does not exist: #{cache_root}")
|
|
1939
|
-
return 0
|
|
1940
|
-
end
|
|
1941
|
-
program_dirs = Dir.glob(File.join(cache_root, "programs", "*", "*")).select { |d| File.directory?(d) }
|
|
1942
|
-
binary_files = Dir.glob(File.join(cache_root, "binaries", "*", "*", "binary")).select { |f| File.file?(f) }
|
|
1943
|
-
total_size = (program_dirs + binary_files).sum { |p|
|
|
1944
|
-
File.file?(p) ? File.size(p) : Dir.glob(File.join(p, "**", "*")).sum { |f| File.file?(f) ? File.size(f) : 0 }
|
|
1945
|
-
}
|
|
1946
|
-
@out.puts("cache #{program_dirs.length} programs, #{binary_files.length} binaries (#{format_size(total_size)})")
|
|
1947
|
-
@out.puts(" root #{cache_root}")
|
|
1948
|
-
0
|
|
1949
|
-
else
|
|
1950
|
-
@err.puts("unknown cache subcommand #{subcommand}")
|
|
1951
|
-
print_command_help("cache", @err)
|
|
1952
|
-
1
|
|
1953
|
-
end
|
|
257
|
+
def invalid_color(value)
|
|
258
|
+
@err.puts("--color must be auto, always, or never#{value ? " (got #{value})" : ''}")
|
|
259
|
+
false
|
|
1954
260
|
end
|
|
1955
261
|
|
|
1956
|
-
def
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
return 1
|
|
262
|
+
def error_color?(io)
|
|
263
|
+
case @color
|
|
264
|
+
when :always then true
|
|
265
|
+
when :never then false
|
|
266
|
+
else io.respond_to?(:tty?) && io.tty?
|
|
1962
267
|
end
|
|
1963
|
-
|
|
1964
|
-
@out.puts(completion_script(shell))
|
|
1965
|
-
0
|
|
1966
268
|
end
|
|
1967
269
|
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
when "bash"
|
|
1972
|
-
[
|
|
1973
|
-
"# mtc bash completion. Source this file or install it into your bash",
|
|
1974
|
-
"# completion directory (e.g. /etc/bash_completion.d/mtc).",
|
|
1975
|
-
"_mtc() {",
|
|
1976
|
-
%( local cur="${COMP_WORDS[COMP_CWORD]}"),
|
|
1977
|
-
%( if [ "${COMP_CWORD}" -eq 1 ]; then),
|
|
1978
|
-
%( COMPREPLY=( $(compgen -W "#{(names + %w[help version]).join(' ')}" -- "${cur}") )),
|
|
1979
|
-
" fi",
|
|
1980
|
-
"}",
|
|
1981
|
-
"complete -F _mtc mtc",
|
|
1982
|
-
].join("\n")
|
|
1983
|
-
when "zsh"
|
|
1984
|
-
lines = ["#compdef mtc", "# mtc zsh completion. Install onto your $fpath as _mtc.", "_mtc() {", " local -a commands", " commands=("]
|
|
1985
|
-
COMMANDS.each { |name, summary| lines << " '#{name}:#{summary}'" }
|
|
1986
|
-
lines.concat([" )", " if (( CURRENT == 2 )); then", " _describe 'mtc command' commands", " fi", "}", %(_mtc "$@")])
|
|
1987
|
-
lines.join("\n")
|
|
1988
|
-
when "fish"
|
|
1989
|
-
lines = ["# mtc fish completion. Install into ~/.config/fish/completions/mtc.fish."]
|
|
1990
|
-
COMMANDS.each do |name, summary|
|
|
1991
|
-
lines << "complete -c mtc -f -n '__fish_use_subcommand' -a #{name} -d '#{summary}'"
|
|
1992
|
-
end
|
|
1993
|
-
lines.join("\n")
|
|
1994
|
-
end
|
|
270
|
+
# Prints an informational/progress line unless --quiet was given.
|
|
271
|
+
def info(message)
|
|
272
|
+
@out.puts(message) unless @quiet
|
|
1995
273
|
end
|
|
1996
274
|
|
|
1997
275
|
def parse_build_options(allow_clean: false)
|
|
@@ -2097,68 +375,6 @@ module MilkTea
|
|
|
2097
375
|
options
|
|
2098
376
|
end
|
|
2099
377
|
|
|
2100
|
-
def parse_format_options
|
|
2101
|
-
options = {
|
|
2102
|
-
check: false,
|
|
2103
|
-
write: false,
|
|
2104
|
-
mode: :safe,
|
|
2105
|
-
max_line_length: nil,
|
|
2106
|
-
profile: false,
|
|
2107
|
-
}
|
|
2108
|
-
input_paths = []
|
|
2109
|
-
|
|
2110
|
-
until @argv.empty?
|
|
2111
|
-
option = @argv.shift
|
|
2112
|
-
if option.start_with?("-")
|
|
2113
|
-
case option
|
|
2114
|
-
when "--check"
|
|
2115
|
-
options[:check] = true
|
|
2116
|
-
when "--write", "-w"
|
|
2117
|
-
options[:write] = true
|
|
2118
|
-
when "--preserve"
|
|
2119
|
-
options[:mode] = :preserve
|
|
2120
|
-
when "--canonical"
|
|
2121
|
-
options[:mode] = :canonical
|
|
2122
|
-
when "--safe"
|
|
2123
|
-
options[:mode] = :safe
|
|
2124
|
-
when "--tidy"
|
|
2125
|
-
options[:mode] = :tidy
|
|
2126
|
-
when "--max-line-length"
|
|
2127
|
-
value = @argv.shift
|
|
2128
|
-
return missing_option_value(option) unless value
|
|
2129
|
-
|
|
2130
|
-
line_length = Integer(value, exception: false)
|
|
2131
|
-
unless line_length && line_length.positive?
|
|
2132
|
-
@err.puts("--max-line-length must be a positive integer")
|
|
2133
|
-
print_usage(@err)
|
|
2134
|
-
return nil
|
|
2135
|
-
end
|
|
2136
|
-
|
|
2137
|
-
options[:max_line_length] = line_length
|
|
2138
|
-
when "--timings"
|
|
2139
|
-
options[:profile] = true
|
|
2140
|
-
when "--"
|
|
2141
|
-
input_paths.concat(@argv)
|
|
2142
|
-
@argv.clear
|
|
2143
|
-
else
|
|
2144
|
-
@err.puts("unknown format option #{option}")
|
|
2145
|
-
print_usage(@err)
|
|
2146
|
-
return nil
|
|
2147
|
-
end
|
|
2148
|
-
else
|
|
2149
|
-
input_paths << option
|
|
2150
|
-
end
|
|
2151
|
-
end
|
|
2152
|
-
|
|
2153
|
-
if options[:check] && options[:write]
|
|
2154
|
-
@err.puts("format options --check and --write cannot be combined")
|
|
2155
|
-
print_usage(@err)
|
|
2156
|
-
return nil
|
|
2157
|
-
end
|
|
2158
|
-
|
|
2159
|
-
{ options:, input_paths: }
|
|
2160
|
-
end
|
|
2161
|
-
|
|
2162
378
|
def ensure_known_source_operands!(command, operands)
|
|
2163
379
|
return true unless operands.any? { |arg| arg.start_with?("-") }
|
|
2164
380
|
|
|
@@ -2333,14 +549,6 @@ module MilkTea
|
|
|
2333
549
|
end
|
|
2334
550
|
end
|
|
2335
551
|
|
|
2336
|
-
def lint_sema_facts_for(source, path, locked: false)
|
|
2337
|
-
ast = Parser.parse(source, path: path)
|
|
2338
|
-
imported_modules = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform).imported_modules_for_ast(ast, importer_path: path)
|
|
2339
|
-
SemanticAnalyzer.tooling_snapshot(ast, imported_modules: imported_modules, path: path).facts
|
|
2340
|
-
rescue MilkTea::LexError, MilkTea::ParseError, SemanticError, ModuleLoadError
|
|
2341
|
-
nil
|
|
2342
|
-
end
|
|
2343
|
-
|
|
2344
552
|
def extract_include_paths!
|
|
2345
553
|
remaining = []
|
|
2346
554
|
i = 0
|