mt-lang 0.2.0
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 +7 -0
- data/.ruby-version +1 -0
- data/AUTHORS +3 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +21 -0
- data/README.md +1208 -0
- data/Rakefile +332 -0
- data/bin/mtc +8 -0
- data/bin/profile-mtc-checks +133 -0
- data/bin/tracy-profiler +0 -0
- data/docs/build-guide.md +536 -0
- data/docs/index.html +2778 -0
- data/docs/language-design.md +1519 -0
- data/docs/language-manual.md +1534 -0
- data/lib/milk_tea/base.rb +49 -0
- data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
- data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
- data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
- data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
- data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
- data/lib/milk_tea/bindings/bindgen.rb +207 -0
- data/lib/milk_tea/bindings/cli.rb +121 -0
- data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
- data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
- data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
- data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
- data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
- data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
- data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
- data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
- data/lib/milk_tea/bindings/steamworks.rb +629 -0
- data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
- data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
- data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
- data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
- data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
- data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
- data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
- data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
- data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
- data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
- data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
- data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
- data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
- data/lib/milk_tea/bindings.rb +23 -0
- data/lib/milk_tea/core/ast.rb +311 -0
- data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
- data/lib/milk_tea/core/binding_types.rb +18 -0
- data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
- data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
- data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
- data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
- data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
- data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
- data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
- data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
- data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
- data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
- data/lib/milk_tea/core/c_backend/statements.rb +563 -0
- data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
- data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
- data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
- data/lib/milk_tea/core/c_backend.rb +287 -0
- data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
- data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
- data/lib/milk_tea/core/compile_time.rb +329 -0
- data/lib/milk_tea/core/control_flow/builder.rb +582 -0
- data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
- data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
- data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
- data/lib/milk_tea/core/control_flow/graph.rb +90 -0
- data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
- data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
- data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
- data/lib/milk_tea/core/control_flow/termination.rb +31 -0
- data/lib/milk_tea/core/control_flow.rb +34 -0
- data/lib/milk_tea/core/cst.rb +48 -0
- data/lib/milk_tea/core/cst_builder.rb +19 -0
- data/lib/milk_tea/core/ir.rb +85 -0
- data/lib/milk_tea/core/keywords.rb +97 -0
- data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
- data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
- data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
- data/lib/milk_tea/core/lexer/indentation.rb +62 -0
- data/lib/milk_tea/core/lexer/numbers.rb +96 -0
- data/lib/milk_tea/core/lexer/recovery.rb +32 -0
- data/lib/milk_tea/core/lexer/strings.rb +167 -0
- data/lib/milk_tea/core/lexer/symbols.rb +71 -0
- data/lib/milk_tea/core/lexer/trivia.rb +53 -0
- data/lib/milk_tea/core/lexer.rb +430 -0
- data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
- data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
- data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
- data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
- data/lib/milk_tea/core/lowering/async.rb +714 -0
- data/lib/milk_tea/core/lowering/block.rb +1052 -0
- data/lib/milk_tea/core/lowering/calls.rb +1565 -0
- data/lib/milk_tea/core/lowering/declarations.rb +214 -0
- data/lib/milk_tea/core/lowering/dyn.rb +206 -0
- data/lib/milk_tea/core/lowering/events.rb +1054 -0
- data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
- data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
- data/lib/milk_tea/core/lowering/functions.rb +242 -0
- data/lib/milk_tea/core/lowering/loops.rb +1087 -0
- data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
- data/lib/milk_tea/core/lowering/proc.rb +419 -0
- data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
- data/lib/milk_tea/core/lowering/scans.rb +220 -0
- data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
- data/lib/milk_tea/core/lowering/utils.rb +1453 -0
- data/lib/milk_tea/core/lowering.rb +378 -0
- data/lib/milk_tea/core/module_binder.rb +181 -0
- data/lib/milk_tea/core/module_loader/errors.rb +21 -0
- data/lib/milk_tea/core/module_loader.rb +479 -0
- data/lib/milk_tea/core/module_path_resolver.rb +153 -0
- data/lib/milk_tea/core/module_roots.rb +88 -0
- data/lib/milk_tea/core/parser/attributes.rb +71 -0
- data/lib/milk_tea/core/parser/blocks.rb +119 -0
- data/lib/milk_tea/core/parser/declarations.rb +749 -0
- data/lib/milk_tea/core/parser/expressions.rb +624 -0
- data/lib/milk_tea/core/parser/recovery.rb +131 -0
- data/lib/milk_tea/core/parser/statements.rb +756 -0
- data/lib/milk_tea/core/parser/types.rb +271 -0
- data/lib/milk_tea/core/parser.rb +400 -0
- data/lib/milk_tea/core/prelude_installer.rb +29 -0
- data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
- data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
- data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
- data/lib/milk_tea/core/pretty_printer.rb +17 -0
- data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
- data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
- data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
- data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
- data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
- data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
- data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
- data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
- data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
- data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
- data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
- data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
- data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
- data/lib/milk_tea/core/token.rb +26 -0
- data/lib/milk_tea/core/token_stream.rb +30 -0
- data/lib/milk_tea/core/types/layout.rb +243 -0
- data/lib/milk_tea/core/types/predicates.rb +609 -0
- data/lib/milk_tea/core/types/registry.rb +83 -0
- data/lib/milk_tea/core/types/types.rb +1696 -0
- data/lib/milk_tea/core/types/visitor.rb +442 -0
- data/lib/milk_tea/core.rb +25 -0
- data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
- data/lib/milk_tea/dap/protocol.rb +58 -0
- data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
- data/lib/milk_tea/dap/server/debug_map.rb +291 -0
- data/lib/milk_tea/dap/server/handlers.rb +374 -0
- data/lib/milk_tea/dap/server/launch.rb +261 -0
- data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
- data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
- data/lib/milk_tea/dap/server/utilities.rb +160 -0
- data/lib/milk_tea/dap/server/wire.rb +55 -0
- data/lib/milk_tea/dap/server.rb +131 -0
- data/lib/milk_tea/dap/session.rb +152 -0
- data/lib/milk_tea/dap.rb +6 -0
- data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
- data/lib/milk_tea/lsp/diagnostics.rb +611 -0
- data/lib/milk_tea/lsp/protocol.rb +104 -0
- data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
- data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
- data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
- data/lib/milk_tea/lsp/server/completion.rb +1099 -0
- data/lib/milk_tea/lsp/server/configuration.rb +167 -0
- data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
- data/lib/milk_tea/lsp/server/definition.rb +779 -0
- data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
- data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
- data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
- data/lib/milk_tea/lsp/server/formatting.rb +575 -0
- data/lib/milk_tea/lsp/server/hover.rb +1465 -0
- data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
- data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
- data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
- data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
- data/lib/milk_tea/lsp/server/progress.rb +47 -0
- data/lib/milk_tea/lsp/server/references.rb +433 -0
- data/lib/milk_tea/lsp/server/rename.rb +598 -0
- data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
- data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
- data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
- data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
- data/lib/milk_tea/lsp/server/utilities.rb +520 -0
- data/lib/milk_tea/lsp/server.rb +415 -0
- data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
- data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
- data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
- data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
- data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
- data/lib/milk_tea/lsp/workspace/store.rb +154 -0
- data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
- data/lib/milk_tea/lsp/workspace.rb +127 -0
- data/lib/milk_tea/lsp.rb +13 -0
- data/lib/milk_tea/packages/atomic_write.rb +63 -0
- data/lib/milk_tea/packages/dependency_solver.rb +194 -0
- data/lib/milk_tea/packages/graph.rb +139 -0
- data/lib/milk_tea/packages/lock.rb +421 -0
- data/lib/milk_tea/packages/manager_cli.rb +485 -0
- data/lib/milk_tea/packages/manifest.rb +356 -0
- data/lib/milk_tea/packages/manifest_editor.rb +134 -0
- data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
- data/lib/milk_tea/packages/registry_store.rb +420 -0
- data/lib/milk_tea/packages/services.rb +41 -0
- data/lib/milk_tea/packages/source_cache.rb +71 -0
- data/lib/milk_tea/packages/source_fetcher.rb +124 -0
- data/lib/milk_tea/packages/source_resolver.rb +389 -0
- data/lib/milk_tea/packages/version.rb +164 -0
- data/lib/milk_tea/packages.rb +16 -0
- data/lib/milk_tea/tooling/asset_pack.rb +141 -0
- data/lib/milk_tea/tooling/build.rb +1124 -0
- data/lib/milk_tea/tooling/build_cache.rb +240 -0
- data/lib/milk_tea/tooling/cli.rb +2688 -0
- data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
- data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
- data/lib/milk_tea/tooling/debug_map.rb +248 -0
- data/lib/milk_tea/tooling/docs_app.rb +369 -0
- data/lib/milk_tea/tooling/error_formatter.rb +86 -0
- data/lib/milk_tea/tooling/formatter.rb +787 -0
- data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
- data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
- data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
- data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
- data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
- data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
- data/lib/milk_tea/tooling/linter/rules.rb +593 -0
- data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
- data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
- data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
- data/lib/milk_tea/tooling/linter.rb +798 -0
- data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
- data/lib/milk_tea/tooling/public/css/docs.css +166 -0
- data/lib/milk_tea/tooling/public/js/docs.js +94 -0
- data/lib/milk_tea/tooling/run.rb +408 -0
- data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
- data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
- data/lib/milk_tea/tooling/views/404.erb +7 -0
- data/lib/milk_tea/tooling/views/index.erb +87 -0
- data/lib/milk_tea/tooling/views/layout.erb +69 -0
- data/lib/milk_tea/tooling/views/module.erb +97 -0
- data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
- data/lib/milk_tea/tooling.rb +20 -0
- data/lib/milk_tea.rb +8 -0
- metadata +426 -0
|
@@ -0,0 +1,2688 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pp"
|
|
4
|
+
require "json"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "stringio"
|
|
7
|
+
require "tempfile"
|
|
8
|
+
require "timeout"
|
|
9
|
+
require "tmpdir"
|
|
10
|
+
|
|
11
|
+
module MilkTea
|
|
12
|
+
class CLI
|
|
13
|
+
GENERATED_FILE_HEADER_PREFIX = "# generated by mtc".freeze
|
|
14
|
+
|
|
15
|
+
# Single source of truth for the top-level command set: used by the help
|
|
16
|
+
# overview and shell-completion generation so they cannot drift.
|
|
17
|
+
COMMANDS = [
|
|
18
|
+
["check", "Type-check and lint source files"],
|
|
19
|
+
["build", "Compile a source file or package"],
|
|
20
|
+
["run", "Build and execute a program"],
|
|
21
|
+
["run-module", "Resolve, build, and run a module by name"],
|
|
22
|
+
["test", "Discover and run @[test] functions"],
|
|
23
|
+
["new", "Scaffold a new package"],
|
|
24
|
+
["format", "Format source files in place or check formatting"],
|
|
25
|
+
["lint", "Lint source files and report warnings"],
|
|
26
|
+
["lex", "Print the lexer token stream"],
|
|
27
|
+
["parse", "Parse source and print the AST"],
|
|
28
|
+
["lower", "Lower source to IR and print it"],
|
|
29
|
+
["emit-c", "Compile source to C and print it"],
|
|
30
|
+
["debug", "Print tokens, AST, facts, and diagnostics"],
|
|
31
|
+
["deps", "Manage package dependencies"],
|
|
32
|
+
["toolchain", "Manage the native toolchain"],
|
|
33
|
+
["bindgen", "Generate a binding module from a C header"],
|
|
34
|
+
["cache", "Inspect and manage the build cache"],
|
|
35
|
+
["docs", "Serve the local documentation site"],
|
|
36
|
+
["snapshot", "Render a highlighted HTML snapshot"],
|
|
37
|
+
["lsp", "Start the Language Server Protocol server"],
|
|
38
|
+
["dap", "Start the Debug Adapter Protocol server"],
|
|
39
|
+
["completions", "Print a shell completion script"],
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
def self.start(argv = ARGV, out: $stdout, err: $stderr, source_overrides: nil, build_frontend: nil)
|
|
43
|
+
new(argv, out:, err:, source_overrides:, build_frontend:).start
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(argv, out:, err:, source_overrides: nil, build_frontend: nil)
|
|
47
|
+
@argv = argv.dup
|
|
48
|
+
@out = out
|
|
49
|
+
@err = err
|
|
50
|
+
@include_module_roots = []
|
|
51
|
+
@include_path_flags = []
|
|
52
|
+
@ambient_module_roots = MilkTea::ModuleRoots.roots_for_path(Dir.pwd)
|
|
53
|
+
@source_overrides = normalize_source_overrides(source_overrides)
|
|
54
|
+
@build_frontend = build_frontend
|
|
55
|
+
@color = :auto
|
|
56
|
+
@verbose = false
|
|
57
|
+
@quiet = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def start
|
|
61
|
+
return 1 unless extract_global_options!
|
|
62
|
+
|
|
63
|
+
extract_include_paths!
|
|
64
|
+
command = @argv.shift
|
|
65
|
+
|
|
66
|
+
if version_request?(command)
|
|
67
|
+
@out.puts("mtc #{MilkTea::VERSION}")
|
|
68
|
+
return 0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if help_request?(command)
|
|
72
|
+
topic = command == "help" ? @argv.shift : nil
|
|
73
|
+
if topic
|
|
74
|
+
print_command_help(topic, @out)
|
|
75
|
+
else
|
|
76
|
+
print_help(@out)
|
|
77
|
+
end
|
|
78
|
+
return 0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if command && @include_module_roots.any? && !command_supports_include_paths?(command)
|
|
82
|
+
@err.puts("unknown option #{@include_path_flags.first} for #{command}")
|
|
83
|
+
print_usage(@err)
|
|
84
|
+
return 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if @argv.first == "--help" || @argv.first == "-h"
|
|
88
|
+
@argv.shift
|
|
89
|
+
print_command_help(command, @out)
|
|
90
|
+
return 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
case command
|
|
94
|
+
when "lex"
|
|
95
|
+
lex_command
|
|
96
|
+
when "parse"
|
|
97
|
+
parse_command
|
|
98
|
+
when "format"
|
|
99
|
+
format_command
|
|
100
|
+
when "lint"
|
|
101
|
+
lint_command
|
|
102
|
+
when "check"
|
|
103
|
+
check_command
|
|
104
|
+
when "lower"
|
|
105
|
+
lower_command
|
|
106
|
+
when "emit-c"
|
|
107
|
+
emit_c_command
|
|
108
|
+
when "build"
|
|
109
|
+
build_command
|
|
110
|
+
when "test"
|
|
111
|
+
test_command
|
|
112
|
+
when "run"
|
|
113
|
+
run_command
|
|
114
|
+
when "run-module"
|
|
115
|
+
app_command
|
|
116
|
+
when "new"
|
|
117
|
+
new_command
|
|
118
|
+
when "debug"
|
|
119
|
+
debug_command
|
|
120
|
+
when "toolchain"
|
|
121
|
+
toolchain_command
|
|
122
|
+
when "deps"
|
|
123
|
+
deps_command
|
|
124
|
+
when "bindgen"
|
|
125
|
+
bindgen_command
|
|
126
|
+
when "cache"
|
|
127
|
+
cache_command
|
|
128
|
+
when "docs"
|
|
129
|
+
docs_command
|
|
130
|
+
when "snapshot"
|
|
131
|
+
snapshot_command
|
|
132
|
+
when "lsp"
|
|
133
|
+
lsp_command
|
|
134
|
+
when "dap"
|
|
135
|
+
dap_command
|
|
136
|
+
when "completions"
|
|
137
|
+
completions_command
|
|
138
|
+
else
|
|
139
|
+
print_usage(@err)
|
|
140
|
+
1
|
|
141
|
+
end
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
raise unless handled_cli_error?(e)
|
|
144
|
+
|
|
145
|
+
@err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
|
|
146
|
+
1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def version_request?(command)
|
|
152
|
+
%w[version --version -V].include?(command)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def help_request?(command)
|
|
156
|
+
command.nil? || %w[help --help -h].include?(command)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Pulls global options that may appear before the first `--` separator out of
|
|
160
|
+
# @argv so each subcommand parser sees a clean argument list. Stops at `--` so
|
|
161
|
+
# arguments forwarded to a run target (e.g. `mtc run app -- --verbose`) are
|
|
162
|
+
# preserved verbatim.
|
|
163
|
+
def extract_global_options!
|
|
164
|
+
remaining = []
|
|
165
|
+
forwarding = false
|
|
166
|
+
i = 0
|
|
167
|
+
while i < @argv.length
|
|
168
|
+
arg = @argv[i]
|
|
169
|
+
if forwarding
|
|
170
|
+
remaining << arg
|
|
171
|
+
i += 1
|
|
172
|
+
next
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
case arg
|
|
176
|
+
when "--"
|
|
177
|
+
forwarding = true
|
|
178
|
+
remaining << arg
|
|
179
|
+
i += 1
|
|
180
|
+
when "-v", "--verbose"
|
|
181
|
+
@verbose = true
|
|
182
|
+
i += 1
|
|
183
|
+
when "-q", "--quiet"
|
|
184
|
+
@quiet = true
|
|
185
|
+
i += 1
|
|
186
|
+
when "--color"
|
|
187
|
+
value = @argv[i + 1]
|
|
188
|
+
return invalid_color(value) unless valid_color?(value)
|
|
189
|
+
|
|
190
|
+
@color = value.to_sym
|
|
191
|
+
i += 2
|
|
192
|
+
when /\A--color=(.*)\z/
|
|
193
|
+
value = ::Regexp.last_match(1)
|
|
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
|
+
|
|
233
|
+
args = @argv.dup
|
|
234
|
+
@argv = []
|
|
235
|
+
until args.empty?
|
|
236
|
+
arg = args.shift
|
|
237
|
+
next if arg == "--"
|
|
238
|
+
|
|
239
|
+
if path.nil?
|
|
240
|
+
path = arg
|
|
241
|
+
else
|
|
242
|
+
@err.puts("unknown option: #{arg}")
|
|
243
|
+
print_usage(@err)
|
|
244
|
+
return 1
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
unless path
|
|
249
|
+
@err.puts("missing source file path")
|
|
250
|
+
print_usage(@err)
|
|
251
|
+
return 1
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
tokens = Lexer.lex(read_source_file(path), path: path)
|
|
255
|
+
@out.write(PP.pp(tokens, +""))
|
|
256
|
+
0
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def parse_command
|
|
260
|
+
args = @argv.dup
|
|
261
|
+
@argv = []
|
|
262
|
+
until args.empty?
|
|
263
|
+
arg = args.shift
|
|
264
|
+
@argv << arg
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
unless @argv.any?
|
|
268
|
+
@err.puts("missing source file path")
|
|
269
|
+
print_usage(@err)
|
|
270
|
+
return 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
resolution = extract_resolution_flags!
|
|
274
|
+
input_paths = @argv.dup
|
|
275
|
+
return 1 unless ensure_known_source_operands!("parse", input_paths)
|
|
276
|
+
|
|
277
|
+
paths = expand_source_paths(input_paths)
|
|
278
|
+
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
279
|
+
|
|
280
|
+
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
281
|
+
|
|
282
|
+
multiple = paths.length > 1
|
|
283
|
+
paths.each_with_index do |path, index|
|
|
284
|
+
ast = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).load_file(path)
|
|
285
|
+
if multiple
|
|
286
|
+
@out.puts("# --- #{path} ---")
|
|
287
|
+
end
|
|
288
|
+
@out.write(PrettyPrinter.format_ast(ast))
|
|
289
|
+
@out.puts if multiple && index < paths.length - 1
|
|
290
|
+
end
|
|
291
|
+
0
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def format_command
|
|
295
|
+
parsed = parse_format_options
|
|
296
|
+
return 1 unless parsed
|
|
297
|
+
|
|
298
|
+
options = parsed[:options]
|
|
299
|
+
input_paths = parsed[:input_paths]
|
|
300
|
+
|
|
301
|
+
if input_paths.empty?
|
|
302
|
+
@err.puts("missing source file path")
|
|
303
|
+
print_usage(@err)
|
|
304
|
+
return 1
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
paths = expand_source_paths(input_paths)
|
|
308
|
+
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
309
|
+
|
|
310
|
+
multiple_sources = input_paths.length > 1 || input_paths.any? { |path| File.directory?(path) }
|
|
311
|
+
if multiple_sources
|
|
312
|
+
unless options[:check] || options[:write]
|
|
313
|
+
@err.puts("format on multiple sources requires --check or --write")
|
|
314
|
+
print_usage(@err)
|
|
315
|
+
return 1
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
return format_paths(paths, options)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
path = paths.first
|
|
322
|
+
|
|
323
|
+
source = read_source_file(path)
|
|
324
|
+
format_profile = options[:profile] ? Linter::Profile.new : nil
|
|
325
|
+
start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
326
|
+
result = Formatter.check_source(source, path: path, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
|
|
327
|
+
elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
328
|
+
|
|
329
|
+
rc = if options[:check]
|
|
330
|
+
announce_file_action(path, "format-check")
|
|
331
|
+
if result.changed
|
|
332
|
+
info("needs formatting #{path}")
|
|
333
|
+
1
|
|
334
|
+
else
|
|
335
|
+
info("already formatted #{path}")
|
|
336
|
+
0
|
|
337
|
+
end
|
|
338
|
+
elsif options[:write]
|
|
339
|
+
announce_file_action(path, "format-write")
|
|
340
|
+
if result.changed
|
|
341
|
+
File.write(path, result.formatted_source)
|
|
342
|
+
info("formatted #{path}")
|
|
343
|
+
else
|
|
344
|
+
info("already formatted #{path}")
|
|
345
|
+
end
|
|
346
|
+
0
|
|
347
|
+
else
|
|
348
|
+
@out.write(result.formatted_source)
|
|
349
|
+
0
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
print_file_profiles([{ path:, total_ms: elapsed_ms, profile: format_profile }], "format") if options[:profile]
|
|
353
|
+
rc
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def format_paths(paths, options)
|
|
357
|
+
format_profiles = []
|
|
358
|
+
if options[:check]
|
|
359
|
+
needs_fmt = []
|
|
360
|
+
paths.each do |p|
|
|
361
|
+
announce_file_action(p, "format-check")
|
|
362
|
+
format_profile = options[:profile] ? Linter::Profile.new : nil
|
|
363
|
+
start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
364
|
+
result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
|
|
365
|
+
elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
366
|
+
format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
|
|
367
|
+
needs_fmt << p if result.changed
|
|
368
|
+
end
|
|
369
|
+
print_file_profiles(format_profiles, "format") if options[:profile]
|
|
370
|
+
if needs_fmt.empty?
|
|
371
|
+
info("all #{paths.size} file(s) already formatted")
|
|
372
|
+
return 0
|
|
373
|
+
end
|
|
374
|
+
needs_fmt.each { |p| info("needs formatting #{p}") }
|
|
375
|
+
info("#{needs_fmt.size} file(s) need formatting")
|
|
376
|
+
return 1
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# --write
|
|
380
|
+
changed = 0
|
|
381
|
+
paths.each do |p|
|
|
382
|
+
announce_file_action(p, "format-write")
|
|
383
|
+
format_profile = options[:profile] ? Linter::Profile.new : nil
|
|
384
|
+
start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
385
|
+
result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
|
|
386
|
+
elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
387
|
+
format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
|
|
388
|
+
if result.changed
|
|
389
|
+
File.write(p, result.formatted_source)
|
|
390
|
+
info("formatted #{p}")
|
|
391
|
+
changed += 1
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
print_file_profiles(format_profiles, "format") if options[:profile]
|
|
395
|
+
info("formatted #{changed} of #{paths.size} file(s)")
|
|
396
|
+
0
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def lint_command
|
|
400
|
+
resolution = { locked: false, frozen: false }
|
|
401
|
+
select = nil
|
|
402
|
+
ignore = nil
|
|
403
|
+
fix = false
|
|
404
|
+
init = false
|
|
405
|
+
ignore_generated = false
|
|
406
|
+
profile = false
|
|
407
|
+
input_paths = []
|
|
408
|
+
until @argv.empty?
|
|
409
|
+
arg = @argv.shift
|
|
410
|
+
unless arg.start_with?("--")
|
|
411
|
+
input_paths << arg
|
|
412
|
+
next
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
flag = arg
|
|
416
|
+
case flag
|
|
417
|
+
when "--select"
|
|
418
|
+
arg = @argv.shift
|
|
419
|
+
unless arg
|
|
420
|
+
@err.puts("--select requires a comma-separated list of rule codes")
|
|
421
|
+
return 1
|
|
422
|
+
end
|
|
423
|
+
select = arg.split(",").map(&:strip).to_set
|
|
424
|
+
when "--ignore"
|
|
425
|
+
arg = @argv.shift
|
|
426
|
+
unless arg
|
|
427
|
+
@err.puts("--ignore requires a comma-separated list of rule codes")
|
|
428
|
+
return 1
|
|
429
|
+
end
|
|
430
|
+
ignore = arg.split(",").map(&:strip).to_set
|
|
431
|
+
when "--fix"
|
|
432
|
+
fix = true
|
|
433
|
+
when "--init"
|
|
434
|
+
init = true
|
|
435
|
+
when "--locked"
|
|
436
|
+
resolution[:locked] = true
|
|
437
|
+
when "--frozen"
|
|
438
|
+
resolution[:locked] = true
|
|
439
|
+
resolution[:frozen] = true
|
|
440
|
+
when "--ignore-generated"
|
|
441
|
+
ignore_generated = true
|
|
442
|
+
when "--timings"
|
|
443
|
+
profile = true
|
|
444
|
+
when "--"
|
|
445
|
+
input_paths.concat(@argv)
|
|
446
|
+
@argv.clear
|
|
447
|
+
else
|
|
448
|
+
@err.puts("unknown lint flag: #{flag}")
|
|
449
|
+
return 1
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
if init
|
|
454
|
+
if input_paths.empty? && !select && !ignore && !fix && !resolution[:locked] && !resolution[:frozen]
|
|
455
|
+
return init_lint_config
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
@err.puts("--init does not accept source paths or lint options")
|
|
459
|
+
return 1
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
if input_paths.empty?
|
|
463
|
+
@err.puts("missing source file path")
|
|
464
|
+
print_usage(@err)
|
|
465
|
+
return 1
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
paths = input_paths.flat_map do |path|
|
|
469
|
+
if File.directory?(path)
|
|
470
|
+
Dir.glob(File.join(path, "**/*.mt")).sort
|
|
471
|
+
else
|
|
472
|
+
[path]
|
|
473
|
+
end
|
|
474
|
+
end.uniq
|
|
475
|
+
|
|
476
|
+
if paths.empty?
|
|
477
|
+
label = input_paths.length == 1 ? input_paths.first : input_paths.join(", ")
|
|
478
|
+
@out.puts("no .mt files found in #{label}")
|
|
479
|
+
return 0
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
483
|
+
|
|
484
|
+
if fix
|
|
485
|
+
lint_profiles = []
|
|
486
|
+
paths.each do |p|
|
|
487
|
+
announce_file_action(p, "lint-fix")
|
|
488
|
+
source = read_source_file(p)
|
|
489
|
+
if ignore_generated && generated_source?(source)
|
|
490
|
+
@out.puts("ignored generated #{p}")
|
|
491
|
+
next
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
facts = lint_sema_facts_for(source, p, locked: resolution[:locked])
|
|
495
|
+
prof = profile ? Linter::Profile.new : nil
|
|
496
|
+
start_time = profile ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
497
|
+
|
|
498
|
+
fixed = Linter.fix_source(
|
|
499
|
+
source,
|
|
500
|
+
path: p,
|
|
501
|
+
sema_facts: facts,
|
|
502
|
+
select:,
|
|
503
|
+
ignore:,
|
|
504
|
+
profile: prof,
|
|
505
|
+
)
|
|
506
|
+
total_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
507
|
+
lint_profiles << { path: p, profile: prof, mode: :pre_fix_scan, total_ms: } if prof
|
|
508
|
+
if fixed != source
|
|
509
|
+
File.write(p, fixed)
|
|
510
|
+
@out.puts("fixed #{p}")
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
print_lint_rule_profiles(lint_profiles) if profile
|
|
514
|
+
print_lint_file_profiles(lint_profiles) if profile
|
|
515
|
+
return 0
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
lint_profiles = []
|
|
519
|
+
all_warnings = paths.flat_map do |p|
|
|
520
|
+
announce_file_action(p, "lint")
|
|
521
|
+
source = read_source_file(p)
|
|
522
|
+
next [] if ignore_generated && generated_source?(source)
|
|
523
|
+
|
|
524
|
+
facts = lint_sema_facts_for(source, p, locked: resolution[:locked])
|
|
525
|
+
prof = profile ? Linter::Profile.new : nil
|
|
526
|
+
start_time = profile ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
|
|
527
|
+
warnings = Linter.lint_source(source, path: p, select:, ignore:, sema_facts: facts, profile: prof)
|
|
528
|
+
total_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
|
|
529
|
+
lint_profiles << { path: p, profile: prof, mode: :lint, total_ms: } if prof
|
|
530
|
+
warnings
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
if all_warnings.empty?
|
|
534
|
+
if input_paths.length == 1
|
|
535
|
+
info("clean #{input_paths.first}")
|
|
536
|
+
else
|
|
537
|
+
info("clean #{paths.size} file(s)")
|
|
538
|
+
end
|
|
539
|
+
print_lint_file_profiles(lint_profiles) if profile
|
|
540
|
+
return 0
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
all_warnings.each do |warning|
|
|
544
|
+
@out.puts("#{warning.path}:#{warning.line}: #{warning.code}: #{warning.message}")
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
print_lint_rule_profiles(lint_profiles) if profile
|
|
548
|
+
print_lint_file_profiles(lint_profiles) if profile
|
|
549
|
+
|
|
550
|
+
file_count = all_warnings.map(&:path).uniq.size
|
|
551
|
+
noun = all_warnings.size == 1 ? "warning" : "warnings"
|
|
552
|
+
files_str = file_count == 1 ? "1 file" : "#{file_count} files"
|
|
553
|
+
@out.puts("Found #{all_warnings.size} #{noun} in #{files_str}.")
|
|
554
|
+
1
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def init_lint_config
|
|
558
|
+
path = File.join(Dir.pwd, Linter::DEFAULT_CONFIG_FILE_NAME)
|
|
559
|
+
if File.exist?(path)
|
|
560
|
+
@err.puts("lint config already exists at #{path}")
|
|
561
|
+
return 1
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
File.write(path, Linter.default_config_source)
|
|
565
|
+
info("created #{path}")
|
|
566
|
+
0
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def print_lint_rule_profiles(lint_profiles, limit: 12)
|
|
570
|
+
lint_profiles.each do |entry|
|
|
571
|
+
profile = entry[:profile]
|
|
572
|
+
next unless profile
|
|
573
|
+
|
|
574
|
+
rows = profile.rule_breakdown(limit:, min_ms: 0.0)
|
|
575
|
+
rule_total = profile.total_time_ms(prefix: "rule.")
|
|
576
|
+
overall_total = profile.total_time_ms
|
|
577
|
+
mode_label = entry[:mode] == :pre_fix_scan ? "pre-fix scan" : "lint scan"
|
|
578
|
+
@out.puts("lint profile #{entry[:path]} (#{mode_label}): rules=#{format('%.1f', rule_total)}ms total=#{format('%.1f', overall_total)}ms")
|
|
579
|
+
|
|
580
|
+
if rows.empty?
|
|
581
|
+
@out.puts(" no rule timing data captured")
|
|
582
|
+
next
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
rows.each do |row|
|
|
586
|
+
share = rule_total.positive? ? ((row[:total_ms] / rule_total) * 100.0) : 0.0
|
|
587
|
+
@out.puts(
|
|
588
|
+
" #{row[:code]}: #{row[:count]}x total=#{format('%.1f', row[:total_ms])}ms avg=#{format('%.2f', row[:avg_ms])}ms share=#{format('%.1f', share)}%"
|
|
589
|
+
)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
non_rule_rows = profile.timings_ms
|
|
593
|
+
.filter_map do |name, total_ms|
|
|
594
|
+
next if name.start_with?("rule.")
|
|
595
|
+
next if total_ms < 1.0
|
|
596
|
+
|
|
597
|
+
[name, total_ms]
|
|
598
|
+
end
|
|
599
|
+
.sort_by { |_name, total_ms| -total_ms }
|
|
600
|
+
.first(5)
|
|
601
|
+
.map do |name, total_ms|
|
|
602
|
+
count = profile.counts[name]
|
|
603
|
+
"#{name}:#{count}x/#{format('%.1f', total_ms)}ms"
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
@out.puts(" non-rule hot phases: #{non_rule_rows.join(', ')}") unless non_rule_rows.empty?
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def print_lint_file_profiles(lint_profiles)
|
|
611
|
+
file_entries = lint_profiles.filter_map do |entry|
|
|
612
|
+
total = entry[:total_ms]
|
|
613
|
+
next unless total
|
|
614
|
+
|
|
615
|
+
phases = entry[:profile]&.timings_ms&.reject { |name, _| name.start_with?("rule.") }&.sort_by { |_, ms| -ms }
|
|
616
|
+
{ path: entry[:path], total_ms: total, phases: }
|
|
617
|
+
end
|
|
618
|
+
return if file_entries.empty?
|
|
619
|
+
|
|
620
|
+
sorted = file_entries.sort_by { |e| -e[:total_ms] }
|
|
621
|
+
@out.puts
|
|
622
|
+
@out.puts("Profile (lint): #{sorted.size} file(s)")
|
|
623
|
+
sorted.each do |entry|
|
|
624
|
+
phase_str = entry[:phases]&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 1.0 }&.join(", ")
|
|
625
|
+
detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
|
|
626
|
+
@out.puts(" #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
|
|
627
|
+
end
|
|
628
|
+
total = sorted.sum { |e| e[:total_ms] }
|
|
629
|
+
@out.puts("Total: #{format('%.1f', total)}ms")
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def print_file_profiles(file_profiles, label)
|
|
633
|
+
sorted = file_profiles.sort_by { |fp| -fp[:total_ms] }
|
|
634
|
+
return if sorted.empty?
|
|
635
|
+
|
|
636
|
+
@out.puts
|
|
637
|
+
if sorted.size == 1
|
|
638
|
+
entry = sorted.first
|
|
639
|
+
phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
|
|
640
|
+
phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 0.1 }&.join(", ")
|
|
641
|
+
detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
|
|
642
|
+
@out.puts("#{label} profile #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
|
|
643
|
+
return
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
@out.puts("Profile (#{label}): #{sorted.size} file(s)")
|
|
647
|
+
sorted.each do |entry|
|
|
648
|
+
phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
|
|
649
|
+
phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 1.0 }&.join(", ")
|
|
650
|
+
detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
|
|
651
|
+
@out.puts(" #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
|
|
652
|
+
end
|
|
653
|
+
total = sorted.sum { |fp| fp[:total_ms] }
|
|
654
|
+
@out.puts("Total: #{format('%.1f', total)}ms")
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def check_command
|
|
658
|
+
args = @argv.dup
|
|
659
|
+
@argv = []
|
|
660
|
+
until args.empty?
|
|
661
|
+
arg = args.shift
|
|
662
|
+
@argv << arg
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
unless @argv.any?
|
|
666
|
+
@err.puts("missing source file path")
|
|
667
|
+
print_usage(@err)
|
|
668
|
+
return 1
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
resolution = extract_resolution_flags!
|
|
672
|
+
input_paths = @argv.dup
|
|
673
|
+
return 1 unless ensure_known_source_operands!("check", input_paths)
|
|
674
|
+
|
|
675
|
+
paths = expand_source_paths(input_paths)
|
|
676
|
+
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
677
|
+
|
|
678
|
+
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
679
|
+
|
|
680
|
+
all_diagnostics = []
|
|
681
|
+
paths.each do |path|
|
|
682
|
+
diagnostics, module_name, closure_errors = check_single_reporting_all(path, locked: resolution[:locked])
|
|
683
|
+
closure_errors = [] if paths.length > 1
|
|
684
|
+
diagnostics = sort_by_location(diagnostics)
|
|
685
|
+
|
|
686
|
+
if diagnostics.any? || closure_errors.any?
|
|
687
|
+
source = read_source_file(path)
|
|
688
|
+
diagnostics.each do |d|
|
|
689
|
+
@err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
|
|
690
|
+
end
|
|
691
|
+
closure_errors.each do |d|
|
|
692
|
+
@err.puts(ErrorFormatter.format(d, color: error_color?(@err)))
|
|
693
|
+
end
|
|
694
|
+
all_diagnostics.concat(diagnostics)
|
|
695
|
+
all_diagnostics.concat(closure_errors)
|
|
696
|
+
elsif module_name
|
|
697
|
+
info("checked #{path} as #{module_name}")
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
return 0 if all_diagnostics.empty?
|
|
702
|
+
|
|
703
|
+
error_count = all_diagnostics.count { |d| !d.respond_to?(:severity) || d.severity == :error }
|
|
704
|
+
warning_count = all_diagnostics.count { |d| d.respond_to?(:severity) && d.severity == :warning }
|
|
705
|
+
info_count = all_diagnostics.count { |d| d.respond_to?(:severity) && (d.severity == :info || d.severity == :hint) }
|
|
706
|
+
|
|
707
|
+
@err.puts
|
|
708
|
+
parts = []
|
|
709
|
+
parts << "#{error_count} #{error_count == 1 ? 'error' : 'errors'}" if error_count > 0
|
|
710
|
+
parts << "#{warning_count} #{warning_count == 1 ? 'warning' : 'warnings'}" if warning_count > 0
|
|
711
|
+
parts << "#{info_count} #{info_count == 1 ? 'note' : 'notes'}" if info_count > 0
|
|
712
|
+
body = parts.join("; ")
|
|
713
|
+
if error_count > 0
|
|
714
|
+
@err.puts("error: could not check due to #{body}")
|
|
715
|
+
elsif warning_count > 0
|
|
716
|
+
@err.puts("warning: #{body}")
|
|
717
|
+
end
|
|
718
|
+
final_error_count = error_count + (resolution[:warnings_as_errors] ? warning_count : 0)
|
|
719
|
+
final_error_count > 0 ? 1 : 0
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def check_single_reporting_all(path, locked: false)
|
|
723
|
+
loader = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform)
|
|
724
|
+
resolved_path = File.expand_path(path)
|
|
725
|
+
ast = loader.load_file(resolved_path)
|
|
726
|
+
module_name = ast.module_name.to_s
|
|
727
|
+
|
|
728
|
+
import_result = loader.send(:imported_modules_for_ast_collecting_errors, ast, importer_path: resolved_path)
|
|
729
|
+
errors = import_result.errors.dup
|
|
730
|
+
|
|
731
|
+
analysis = nil
|
|
732
|
+
begin
|
|
733
|
+
result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path)
|
|
734
|
+
errors.concat(result[:errors])
|
|
735
|
+
analysis = result[:analysis]
|
|
736
|
+
rescue SemanticError => e
|
|
737
|
+
errors << e
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
if analysis && errors.empty?
|
|
741
|
+
source = read_source_file(path)
|
|
742
|
+
warnings = Linter.lint_source(source, path: resolved_path, sema_facts: analysis, lint_tier: :full)
|
|
743
|
+
errors.concat(warnings)
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
closure_errors = loader.collecting_path_errors.values.flatten.compact
|
|
747
|
+
[errors, module_name, closure_errors]
|
|
748
|
+
rescue ModuleLoadError, PackageLockError, SemanticError => e
|
|
749
|
+
[[e], nil, []]
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def sort_by_location(errors)
|
|
753
|
+
errors.sort_by do |e|
|
|
754
|
+
actual = e.respond_to?(:error) ? e.error : e
|
|
755
|
+
line = actual.respond_to?(:line) ? actual.line.to_i : 0
|
|
756
|
+
column = actual.respond_to?(:column) ? actual.column.to_i : 0
|
|
757
|
+
[line, column]
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def lower_command
|
|
762
|
+
args = @argv.dup
|
|
763
|
+
@argv = []
|
|
764
|
+
until args.empty?
|
|
765
|
+
arg = args.shift
|
|
766
|
+
@argv << arg
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
unless @argv.any?
|
|
770
|
+
@err.puts("missing source file path")
|
|
771
|
+
print_usage(@err)
|
|
772
|
+
return 1
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
resolution = extract_resolution_flags!
|
|
776
|
+
input_paths = @argv.dup
|
|
777
|
+
return 1 unless ensure_known_source_operands!("lower", input_paths)
|
|
778
|
+
|
|
779
|
+
paths = expand_source_paths(input_paths)
|
|
780
|
+
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
781
|
+
|
|
782
|
+
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
783
|
+
|
|
784
|
+
multiple = paths.length > 1
|
|
785
|
+
paths.each_with_index do |path, index|
|
|
786
|
+
program = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).check_program(path)
|
|
787
|
+
if multiple
|
|
788
|
+
@out.puts("# --- #{path} ---")
|
|
789
|
+
end
|
|
790
|
+
@out.write(PrettyPrinter.format_ir(Lowering.lower(program)))
|
|
791
|
+
@out.puts if multiple && index < paths.length - 1
|
|
792
|
+
end
|
|
793
|
+
0
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def emit_c_command
|
|
797
|
+
args = @argv.dup
|
|
798
|
+
@argv = []
|
|
799
|
+
until args.empty?
|
|
800
|
+
arg = args.shift
|
|
801
|
+
@argv << arg
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
unless @argv.any?
|
|
805
|
+
@err.puts("missing source file path")
|
|
806
|
+
print_usage(@err)
|
|
807
|
+
return 1
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
resolution = extract_resolution_flags!
|
|
811
|
+
input_paths = @argv.dup
|
|
812
|
+
return 1 unless ensure_known_source_operands!("emit-c", input_paths)
|
|
813
|
+
|
|
814
|
+
program_paths = input_paths.map { |p| resolve_program_path(p) }
|
|
815
|
+
return 1 if program_paths.include?(nil)
|
|
816
|
+
|
|
817
|
+
ensure_current_lockfiles!(input_paths) if resolution[:frozen]
|
|
818
|
+
|
|
819
|
+
multiple = program_paths.length > 1
|
|
820
|
+
program_paths.each_with_index do |path, index|
|
|
821
|
+
program = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).check_program(path)
|
|
822
|
+
if multiple
|
|
823
|
+
@out.puts("/* --- #{path} --- */")
|
|
824
|
+
end
|
|
825
|
+
@out.write(CBackend.generate_c(Lowering.lower(program), emit_line_directives: false))
|
|
826
|
+
@out.puts if multiple && index < program_paths.length - 1
|
|
827
|
+
end
|
|
828
|
+
0
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def resolve_program_path(path)
|
|
832
|
+
return path unless File.directory?(path)
|
|
833
|
+
|
|
834
|
+
manifest_path = File.join(path, "package.toml")
|
|
835
|
+
unless File.file?(manifest_path)
|
|
836
|
+
@err.puts("no package.toml found in #{path}")
|
|
837
|
+
return nil
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
manifest = PackageManifest.load(path)
|
|
841
|
+
entry_path = manifest.source_path
|
|
842
|
+
unless entry_path && File.file?(entry_path)
|
|
843
|
+
@err.puts("no build entry found for #{path}")
|
|
844
|
+
return nil
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
entry_path
|
|
848
|
+
rescue PackageManifestError => e
|
|
849
|
+
@err.puts("failed to load package manifest for #{path}: #{e.message}")
|
|
850
|
+
nil
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
def build_command
|
|
854
|
+
path, options = extract_path_and_options(allow_clean: true)
|
|
855
|
+
return 1 unless path
|
|
856
|
+
|
|
857
|
+
if options.delete(:clean)
|
|
858
|
+
cleaned_path = Build.clean(path, output_path: options[:output_path], profile: options[:profile], platform: options[:platform], bundle: options[:bundle], archive: options[:archive])
|
|
859
|
+
info("cleaned #{cleaned_path}")
|
|
860
|
+
return 0
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
frozen = options.delete(:frozen)
|
|
864
|
+
ensure_current_lockfile!(path) if frozen
|
|
865
|
+
locked = options.delete(:locked)
|
|
866
|
+
bundle = options[:bundle]
|
|
867
|
+
package_graph = package_graph_for(path, locked:)
|
|
868
|
+
result = Build.build(path, module_roots: module_roots_for(path, locked:), package_graph:, frontend: @build_frontend, **options.except(:timings))
|
|
869
|
+
if bundle
|
|
870
|
+
info("built #{path} -> #{File.dirname(result.output_path)}")
|
|
871
|
+
info("entry executable #{result.output_path}")
|
|
872
|
+
info(" [cached]") if result.cached
|
|
873
|
+
info("archive #{result.archive_path}") if result.archive_path
|
|
874
|
+
elsif result.cached
|
|
875
|
+
info("built #{path} -> #{result.output_path} [cached]")
|
|
876
|
+
else
|
|
877
|
+
info("built #{path} -> #{result.output_path}")
|
|
878
|
+
end
|
|
879
|
+
info("saved C to #{result.c_path}") if result.c_path
|
|
880
|
+
0
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def test_command
|
|
884
|
+
limits = extract_test_limit_flags!
|
|
885
|
+
return 1 unless limits
|
|
886
|
+
|
|
887
|
+
@test_timeout_seconds, @test_memory_bytes, @test_jobs, @test_sanitize, @test_filter, @test_format = limits
|
|
888
|
+
|
|
889
|
+
path, options = extract_path_and_options
|
|
890
|
+
return 1 unless path
|
|
891
|
+
|
|
892
|
+
frozen = options.delete(:frozen)
|
|
893
|
+
ensure_current_lockfile!(path) if frozen
|
|
894
|
+
locked = options.delete(:locked)
|
|
895
|
+
|
|
896
|
+
return dispatch_test_run(path, options:, locked:) if @test_format == :human
|
|
897
|
+
|
|
898
|
+
buffer = StringIO.new
|
|
899
|
+
real_out = @out
|
|
900
|
+
real_err = @err
|
|
901
|
+
@out = buffer
|
|
902
|
+
@err = buffer
|
|
903
|
+
begin
|
|
904
|
+
exit_code = dispatch_test_run(path, options:, locked:)
|
|
905
|
+
ensure
|
|
906
|
+
@out = real_out
|
|
907
|
+
@err = real_err
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
emit_machine_results(parse_test_output(buffer.string), @test_format, real_out)
|
|
911
|
+
exit_code
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def dispatch_test_run(path, options:, locked:)
|
|
915
|
+
if File.directory?(path)
|
|
916
|
+
run_test_directory(path, options:, locked:)
|
|
917
|
+
elsif File.file?(path)
|
|
918
|
+
if compile_fail_fixture?(File.read(path))
|
|
919
|
+
run_compile_fail_test(path) ? 0 : 1
|
|
920
|
+
else
|
|
921
|
+
run_test_file(path, options:, locked:)
|
|
922
|
+
end
|
|
923
|
+
else
|
|
924
|
+
@err.puts("mtc test: not a file or directory: #{path}")
|
|
925
|
+
1
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
def extract_test_limit_flags!
|
|
930
|
+
timeout_seconds = TEST_RUN_TIMEOUT_SECONDS
|
|
931
|
+
memory_bytes = TEST_RUN_MEMORY_LIMIT_BYTES
|
|
932
|
+
jobs = 1
|
|
933
|
+
sanitize = false
|
|
934
|
+
filter = nil
|
|
935
|
+
format = :human
|
|
936
|
+
remaining = []
|
|
937
|
+
until @argv.empty?
|
|
938
|
+
arg = @argv.shift
|
|
939
|
+
case arg
|
|
940
|
+
when "--timeout"
|
|
941
|
+
value = @argv.shift
|
|
942
|
+
seconds = value && Integer(value, exception: false)
|
|
943
|
+
unless seconds&.positive?
|
|
944
|
+
@err.puts("--timeout requires a positive integer (seconds)")
|
|
945
|
+
return nil
|
|
946
|
+
end
|
|
947
|
+
timeout_seconds = seconds
|
|
948
|
+
when "--mem"
|
|
949
|
+
value = @argv.shift
|
|
950
|
+
megabytes = value && Integer(value, exception: false)
|
|
951
|
+
unless megabytes&.positive?
|
|
952
|
+
@err.puts("--mem requires a positive integer (megabytes)")
|
|
953
|
+
return nil
|
|
954
|
+
end
|
|
955
|
+
memory_bytes = megabytes * 1024 * 1024
|
|
956
|
+
when "--jobs"
|
|
957
|
+
value = @argv.shift
|
|
958
|
+
count = value && Integer(value, exception: false)
|
|
959
|
+
unless count&.positive?
|
|
960
|
+
@err.puts("--jobs requires a positive integer")
|
|
961
|
+
return nil
|
|
962
|
+
end
|
|
963
|
+
jobs = count
|
|
964
|
+
when "--sanitize"
|
|
965
|
+
sanitize = true
|
|
966
|
+
when "-n", "--name"
|
|
967
|
+
value = @argv.shift
|
|
968
|
+
unless value
|
|
969
|
+
@err.puts("-n requires a name substring")
|
|
970
|
+
return nil
|
|
971
|
+
end
|
|
972
|
+
filter = value
|
|
973
|
+
when "--format"
|
|
974
|
+
value = @argv.shift
|
|
975
|
+
unless %w[human tap junit].include?(value)
|
|
976
|
+
@err.puts("--format must be human, tap, or junit")
|
|
977
|
+
return nil
|
|
978
|
+
end
|
|
979
|
+
format = value.to_sym
|
|
980
|
+
when "--"
|
|
981
|
+
remaining << arg
|
|
982
|
+
remaining.concat(@argv)
|
|
983
|
+
@argv.clear
|
|
984
|
+
else
|
|
985
|
+
remaining << arg
|
|
986
|
+
end
|
|
987
|
+
end
|
|
988
|
+
@argv.replace(remaining)
|
|
989
|
+
[timeout_seconds, memory_bytes, jobs, sanitize, filter, format]
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
TestResult = Data.define(:name, :status, :detail)
|
|
993
|
+
|
|
994
|
+
def parse_test_output(text)
|
|
995
|
+
results = []
|
|
996
|
+
current_file = nil
|
|
997
|
+
text.each_line do |raw|
|
|
998
|
+
line = raw.chomp
|
|
999
|
+
case line
|
|
1000
|
+
when /\A# (.+)\z/
|
|
1001
|
+
current_file = ::Regexp.last_match(1)
|
|
1002
|
+
when /\Aok - (.+)\z/
|
|
1003
|
+
results << TestResult.new(name: ::Regexp.last_match(1), status: :pass, detail: nil)
|
|
1004
|
+
when /\Askip - (.+?)(?:: (.*))?\z/
|
|
1005
|
+
results << TestResult.new(name: ::Regexp.last_match(1), status: :skip, detail: ::Regexp.last_match(2))
|
|
1006
|
+
when /\AFAIL - (.+?)(?:: (.*))?\z/
|
|
1007
|
+
results << TestResult.new(name: ::Regexp.last_match(1), status: :fail, detail: ::Regexp.last_match(2))
|
|
1008
|
+
when /\AFAILED - (.+?) \(build error\)\z/
|
|
1009
|
+
results << TestResult.new(name: "#{::Regexp.last_match(1)} (build error)", status: :fail, detail: "build error")
|
|
1010
|
+
when /\Atest run (?:timed out|crashed)/, /\ASUMMARY: \w+Sanitizer/
|
|
1011
|
+
results << TestResult.new(name: "#{current_file || 'test'} (#{line})", status: :fail, detail: line)
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
results
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def emit_machine_results(results, format, out)
|
|
1018
|
+
case format
|
|
1019
|
+
when :tap then emit_tap(results, out)
|
|
1020
|
+
when :junit then emit_junit(results, out)
|
|
1021
|
+
end
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
def emit_tap(results, out)
|
|
1025
|
+
out.puts("TAP version 13")
|
|
1026
|
+
out.puts("1..#{results.length}")
|
|
1027
|
+
results.each_with_index do |result, index|
|
|
1028
|
+
number = index + 1
|
|
1029
|
+
case result.status
|
|
1030
|
+
when :pass
|
|
1031
|
+
out.puts("ok #{number} - #{result.name}")
|
|
1032
|
+
when :skip
|
|
1033
|
+
out.puts("ok #{number} - #{result.name} # SKIP#{result.detail ? " #{result.detail}" : ''}")
|
|
1034
|
+
when :fail
|
|
1035
|
+
out.puts("not ok #{number} - #{result.name}")
|
|
1036
|
+
next unless result.detail
|
|
1037
|
+
|
|
1038
|
+
out.puts(" ---")
|
|
1039
|
+
out.puts(" message: #{result.detail}")
|
|
1040
|
+
out.puts(" ...")
|
|
1041
|
+
end
|
|
1042
|
+
end
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
def emit_junit(results, out)
|
|
1046
|
+
failures = results.count { |result| result.status == :fail }
|
|
1047
|
+
skipped = results.count { |result| result.status == :skip }
|
|
1048
|
+
out.puts(%(<?xml version="1.0" encoding="UTF-8"?>))
|
|
1049
|
+
out.puts(%(<testsuites tests="#{results.length}" failures="#{failures}" skipped="#{skipped}">))
|
|
1050
|
+
out.puts(%( <testsuite name="mtc test" tests="#{results.length}" failures="#{failures}" skipped="#{skipped}">))
|
|
1051
|
+
results.each do |result|
|
|
1052
|
+
name = xml_escape(result.name)
|
|
1053
|
+
case result.status
|
|
1054
|
+
when :pass
|
|
1055
|
+
out.puts(%( <testcase name="#{name}"/>))
|
|
1056
|
+
when :skip
|
|
1057
|
+
out.puts(%( <testcase name="#{name}"><skipped/></testcase>))
|
|
1058
|
+
when :fail
|
|
1059
|
+
out.puts(%( <testcase name="#{name}"><failure message="#{xml_escape(result.detail || 'failed')}"/></testcase>))
|
|
1060
|
+
end
|
|
1061
|
+
end
|
|
1062
|
+
out.puts(" </testsuite>")
|
|
1063
|
+
out.puts("</testsuites>")
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
def xml_escape(value)
|
|
1067
|
+
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
def run_test_directory(directory, options:, locked:)
|
|
1071
|
+
test_files = discover_test_files(directory)
|
|
1072
|
+
if test_files.empty?
|
|
1073
|
+
if @test_filter
|
|
1074
|
+
@out.puts("no tests matched -n '#{@test_filter}' under #{directory}")
|
|
1075
|
+
else
|
|
1076
|
+
@out.puts("no @[test] functions or # expect-error: fixtures found under #{directory}")
|
|
1077
|
+
end
|
|
1078
|
+
return 0
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
jobs = @test_jobs || 1
|
|
1082
|
+
return run_test_files_parallel(test_files, jobs:, options:, locked:) if jobs > 1 && Process.respond_to?(:fork)
|
|
1083
|
+
|
|
1084
|
+
failed = 0
|
|
1085
|
+
test_files.each do |file, kind|
|
|
1086
|
+
@out.puts("# #{file}")
|
|
1087
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1088
|
+
failed += 1 unless run_classified_file(file, kind, options:, locked:).zero?
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
@out.puts("")
|
|
1092
|
+
@out.puts("#{test_files.length} test file(s), #{failed} failed")
|
|
1093
|
+
failed.zero? ? 0 : 1
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
def run_classified_file(file, kind, options:, locked:)
|
|
1097
|
+
if kind == :compile_fail
|
|
1098
|
+
run_compile_fail_test(file) ? 0 : 1
|
|
1099
|
+
else
|
|
1100
|
+
run_test_file_guarded(file, options:, locked:)
|
|
1101
|
+
end
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
def run_compile_fail_test(path)
|
|
1105
|
+
source = File.read(path)
|
|
1106
|
+
expectations = extract_expect_error_directives(source)
|
|
1107
|
+
messages = compile_fail_diagnostics(path)
|
|
1108
|
+
|
|
1109
|
+
if messages.empty?
|
|
1110
|
+
@out.puts("FAIL - #{path} (compile-fail): expected a compile error, but it compiled cleanly")
|
|
1111
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1112
|
+
return false
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
unmatched = expectations.find { |expected| messages.none? { |message| message.include?(expected) } }
|
|
1116
|
+
if unmatched
|
|
1117
|
+
@out.puts("FAIL - #{path} (compile-fail): no diagnostic matched #{unmatched.inspect}")
|
|
1118
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1119
|
+
return false
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
@out.puts("ok - #{path} (compile-fail)")
|
|
1123
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1124
|
+
true
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
def extract_expect_error_directives(source)
|
|
1128
|
+
source.each_line.filter_map do |line|
|
|
1129
|
+
match = line.match(/^\s*#\s*expect-error:\s*(.+?)\s*$/)
|
|
1130
|
+
match && match[1]
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def compile_fail_diagnostics(path)
|
|
1135
|
+
errors, = check_single_reporting_all(path, locked: false)
|
|
1136
|
+
errors
|
|
1137
|
+
.select { |diagnostic| !diagnostic.respond_to?(:severity) || diagnostic.severity == :error }
|
|
1138
|
+
.map { |diagnostic| ErrorFormatter.format(diagnostic, color: false) }
|
|
1139
|
+
rescue StandardError => e
|
|
1140
|
+
raise unless handled_cli_error?(e)
|
|
1141
|
+
|
|
1142
|
+
[ErrorFormatter.format(e, color: false)]
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def run_test_file_guarded(file, options:, locked:)
|
|
1146
|
+
run_test_file(file, options:, locked:)
|
|
1147
|
+
rescue StandardError => e
|
|
1148
|
+
raise unless handled_cli_error?(e)
|
|
1149
|
+
|
|
1150
|
+
@err.puts("FAILED - #{file} (build error)")
|
|
1151
|
+
@err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
|
|
1152
|
+
1
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def run_test_files_parallel(test_files, jobs:, options:, locked:)
|
|
1156
|
+
results = Array.new(test_files.length)
|
|
1157
|
+
result_paths = {}
|
|
1158
|
+
active = {}
|
|
1159
|
+
cursor = 0
|
|
1160
|
+
|
|
1161
|
+
while cursor < test_files.length || !active.empty?
|
|
1162
|
+
while active.size < jobs && cursor < test_files.length
|
|
1163
|
+
index = cursor
|
|
1164
|
+
cursor += 1
|
|
1165
|
+
result_path = File.join(Dir.tmpdir, "mttest_result_#{Process.pid}_#{index}")
|
|
1166
|
+
result_paths[index] = result_path
|
|
1167
|
+
pid = fork do
|
|
1168
|
+
captured = StringIO.new
|
|
1169
|
+
@out = captured
|
|
1170
|
+
@err = captured
|
|
1171
|
+
file, kind = test_files[index]
|
|
1172
|
+
code = run_classified_file(file, kind, options:, locked:)
|
|
1173
|
+
File.binwrite(result_path, [code].pack("N") + captured.string)
|
|
1174
|
+
exit!(0)
|
|
1175
|
+
end
|
|
1176
|
+
active[pid] = index
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
finished_pid, = Process.wait2
|
|
1180
|
+
index = active.delete(finished_pid)
|
|
1181
|
+
next unless index
|
|
1182
|
+
|
|
1183
|
+
path = result_paths[index]
|
|
1184
|
+
data = begin
|
|
1185
|
+
File.binread(path)
|
|
1186
|
+
rescue StandardError
|
|
1187
|
+
(+"").b
|
|
1188
|
+
end
|
|
1189
|
+
File.delete(path) if File.exist?(path)
|
|
1190
|
+
results[index] =
|
|
1191
|
+
if data.bytesize >= 4
|
|
1192
|
+
[data[0, 4].unpack1("N"), data.byteslice(4..).force_encoding(Encoding::UTF_8)]
|
|
1193
|
+
else
|
|
1194
|
+
[1, +""]
|
|
1195
|
+
end
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
failed = 0
|
|
1199
|
+
test_files.each_with_index do |(file, _kind), index|
|
|
1200
|
+
code, output = results[index]
|
|
1201
|
+
@out.puts("# #{file}")
|
|
1202
|
+
@out.write(output.to_s)
|
|
1203
|
+
failed += 1 unless code&.zero?
|
|
1204
|
+
end
|
|
1205
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1206
|
+
@out.puts("")
|
|
1207
|
+
@out.puts("#{test_files.length} test file(s), #{failed} failed")
|
|
1208
|
+
failed.zero? ? 0 : 1
|
|
1209
|
+
end
|
|
1210
|
+
|
|
1211
|
+
def discover_test_files(directory)
|
|
1212
|
+
Dir.glob(File.join(directory, "**", "*.mt")).sort.filter_map do |file|
|
|
1213
|
+
next if File.basename(file).start_with?("__mt_test_runner_")
|
|
1214
|
+
|
|
1215
|
+
kind = classify_test_file(file)
|
|
1216
|
+
kind && [file, kind]
|
|
1217
|
+
end
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def classify_test_file(file)
|
|
1221
|
+
source = File.read(file)
|
|
1222
|
+
if compile_fail_fixture?(source)
|
|
1223
|
+
return nil if @test_filter && !File.basename(file, ".mt").include?(@test_filter)
|
|
1224
|
+
|
|
1225
|
+
return :compile_fail
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1228
|
+
ast = begin
|
|
1229
|
+
MilkTea::Parser.parse(source, path: file)
|
|
1230
|
+
rescue ParseError
|
|
1231
|
+
return nil
|
|
1232
|
+
end
|
|
1233
|
+
has_match = ast.declarations.any? do |decl|
|
|
1234
|
+
decl.is_a?(AST::FunctionDef) && test_attribute?(decl) && matches_filter?(decl.name)
|
|
1235
|
+
end
|
|
1236
|
+
has_match ? :test : nil
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
def compile_fail_fixture?(source)
|
|
1240
|
+
source.match?(/^\s*#\s*expect-error:/)
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
def test_attribute?(decl)
|
|
1244
|
+
decl.attributes.any? { |attribute| attribute.name.parts == ["test"] }
|
|
1245
|
+
end
|
|
1246
|
+
|
|
1247
|
+
def matches_filter?(name)
|
|
1248
|
+
@test_filter.nil? || name.include?(@test_filter)
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
def run_test_file(path, options:, locked:)
|
|
1252
|
+
source = File.read(path)
|
|
1253
|
+
ast = MilkTea::Parser.parse(source, path:)
|
|
1254
|
+
|
|
1255
|
+
if ast.declarations.any? { |decl| decl.is_a?(AST::FunctionDef) && decl.name == "main" }
|
|
1256
|
+
@err.puts("a test file must not define 'main': #{path}")
|
|
1257
|
+
return 1
|
|
1258
|
+
end
|
|
1259
|
+
|
|
1260
|
+
tests = ast.declarations.select { |decl| decl.is_a?(AST::FunctionDef) && test_attribute?(decl) }
|
|
1261
|
+
|
|
1262
|
+
if tests.empty?
|
|
1263
|
+
@out.puts("no @[test] functions found in #{path}")
|
|
1264
|
+
return 0
|
|
1265
|
+
end
|
|
1266
|
+
|
|
1267
|
+
tests = tests.select { |test| matches_filter?(test.name) }
|
|
1268
|
+
return 0 if tests.empty?
|
|
1269
|
+
|
|
1270
|
+
invalid = tests.find { |test| !test.params.empty? }
|
|
1271
|
+
if invalid
|
|
1272
|
+
@err.puts("@[test] function '#{invalid.name}' must take no parameters")
|
|
1273
|
+
return 1
|
|
1274
|
+
end
|
|
1275
|
+
|
|
1276
|
+
testing_import = ast.imports.find { |import| import.path.parts == %w[std testing] }
|
|
1277
|
+
unless testing_import
|
|
1278
|
+
@err.puts("a test file must import std.testing: #{path}")
|
|
1279
|
+
return 1
|
|
1280
|
+
end
|
|
1281
|
+
testing_alias = testing_import.alias_name || testing_import.path.parts.last
|
|
1282
|
+
|
|
1283
|
+
death_tests, normal_tests = tests.partition { |test| expect_fatal_attribute?(test) }
|
|
1284
|
+
|
|
1285
|
+
exit_code = 0
|
|
1286
|
+
|
|
1287
|
+
unless normal_tests.empty?
|
|
1288
|
+
runner_source = source.dup
|
|
1289
|
+
runner_source << "\n\n" << test_runner_main(testing_alias, normal_tests.map(&:name))
|
|
1290
|
+
exit_code = run_synthesized_tests(path, runner_source, options:, locked:)
|
|
1291
|
+
end
|
|
1292
|
+
|
|
1293
|
+
death_tests.each do |death_test|
|
|
1294
|
+
exit_code = 1 unless run_death_test(path, source, death_test.name, options:, locked:)
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
exit_code
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
def expect_fatal_attribute?(decl)
|
|
1301
|
+
decl.attributes.any? { |attribute| attribute.name.parts == ["expect_fatal"] }
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def run_death_test(source_path, source, test_name, options:, locked:)
|
|
1305
|
+
runner_source = source.dup
|
|
1306
|
+
runner_source << "\n\n" << death_test_runner_main(test_name)
|
|
1307
|
+
classification = with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
|
|
1308
|
+
classify_death_test(binary_path)
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
passed = classification == :aborted
|
|
1312
|
+
line =
|
|
1313
|
+
if passed
|
|
1314
|
+
"ok - #{test_name} (expect_fatal)"
|
|
1315
|
+
elsif classification == :timed_out
|
|
1316
|
+
"FAIL - #{test_name} (expect_fatal): timed out"
|
|
1317
|
+
else
|
|
1318
|
+
"FAIL - #{test_name} (expect_fatal): expected a fatal abort, but the test returned"
|
|
1319
|
+
end
|
|
1320
|
+
@out.puts(line)
|
|
1321
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1322
|
+
passed
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
def classify_death_test(binary_path)
|
|
1326
|
+
_output, status, timed_out = spawn_sandboxed(binary_path)
|
|
1327
|
+
return :timed_out if timed_out
|
|
1328
|
+
return :returned if status&.exited? && status.exitstatus&.zero?
|
|
1329
|
+
|
|
1330
|
+
:aborted
|
|
1331
|
+
end
|
|
1332
|
+
|
|
1333
|
+
def death_test_runner_main(test_name)
|
|
1334
|
+
[
|
|
1335
|
+
"function main() -> int:",
|
|
1336
|
+
" match #{test_name}():",
|
|
1337
|
+
" Result.success:",
|
|
1338
|
+
" return 0",
|
|
1339
|
+
" Result.failure:",
|
|
1340
|
+
" return 0",
|
|
1341
|
+
].join("\n") + "\n"
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
def test_runner_main(testing_alias, test_names)
|
|
1345
|
+
lines = ["function main() -> int:"]
|
|
1346
|
+
lines << " var __mt_test_stats = #{testing_alias}.Stats.create()"
|
|
1347
|
+
test_names.each do |name|
|
|
1348
|
+
lines << " __mt_test_stats = #{testing_alias}.record(__mt_test_stats, #{name.inspect}, #{name}())"
|
|
1349
|
+
end
|
|
1350
|
+
lines << " return #{testing_alias}.summarize(__mt_test_stats)"
|
|
1351
|
+
lines.join("\n") + "\n"
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
def run_synthesized_tests(source_path, runner_source, options:, locked:)
|
|
1355
|
+
with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
|
|
1356
|
+
run_test_binary(binary_path)
|
|
1357
|
+
end
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
def with_synthesized_binary(source_path, runner_source, options:, locked:)
|
|
1361
|
+
directory = File.dirname(File.expand_path(source_path))
|
|
1362
|
+
runner_path = File.join(directory, "__mt_test_runner_#{Process.pid}.mt")
|
|
1363
|
+
binary_path = File.join(Dir.tmpdir, "__mt_test_runner_#{Process.pid}")
|
|
1364
|
+
|
|
1365
|
+
File.write(runner_path, runner_source)
|
|
1366
|
+
begin
|
|
1367
|
+
build_opts = options.except(:timings, :output_path, :bundle, :archive)
|
|
1368
|
+
build_opts[:debug_guards] = false
|
|
1369
|
+
Build.build(
|
|
1370
|
+
runner_path,
|
|
1371
|
+
output_path: binary_path,
|
|
1372
|
+
module_roots: module_roots_for(source_path, locked:),
|
|
1373
|
+
package_graph: package_graph_for(source_path, locked:),
|
|
1374
|
+
frontend: @build_frontend,
|
|
1375
|
+
sanitize: @test_sanitize,
|
|
1376
|
+
**build_opts,
|
|
1377
|
+
)
|
|
1378
|
+
yield binary_path
|
|
1379
|
+
ensure
|
|
1380
|
+
File.delete(runner_path) if File.exist?(runner_path)
|
|
1381
|
+
File.delete(binary_path) if File.exist?(binary_path)
|
|
1382
|
+
end
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
TEST_RUN_TIMEOUT_SECONDS = 30
|
|
1386
|
+
TEST_RUN_MEMORY_LIMIT_BYTES = 1024 * 1024 * 1024
|
|
1387
|
+
|
|
1388
|
+
def run_test_binary(binary_path)
|
|
1389
|
+
output, status, timed_out = spawn_sandboxed(binary_path)
|
|
1390
|
+
@out.write(output)
|
|
1391
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1392
|
+
|
|
1393
|
+
if timed_out
|
|
1394
|
+
@err.puts("test run timed out after #{@test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS}s")
|
|
1395
|
+
return 1
|
|
1396
|
+
end
|
|
1397
|
+
if status&.signaled?
|
|
1398
|
+
@err.puts("test run crashed (signal #{status.termsig})")
|
|
1399
|
+
return 1
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
status&.exitstatus || 1
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
def spawn_sandboxed(binary_path)
|
|
1406
|
+
timeout_seconds = @test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS
|
|
1407
|
+
memory_bytes = @test_memory_bytes || TEST_RUN_MEMORY_LIMIT_BYTES
|
|
1408
|
+
reader, writer = IO.pipe
|
|
1409
|
+
spawn_options = { out: writer, err: writer, pgroup: true }
|
|
1410
|
+
spawn_options[:rlimit_as] = memory_bytes unless @test_sanitize
|
|
1411
|
+
pid = Process.spawn(binary_path, **spawn_options)
|
|
1412
|
+
writer.close
|
|
1413
|
+
|
|
1414
|
+
status = nil
|
|
1415
|
+
timed_out = false
|
|
1416
|
+
begin
|
|
1417
|
+
Timeout.timeout(timeout_seconds) { _, status = Process.wait2(pid) }
|
|
1418
|
+
rescue Timeout::Error
|
|
1419
|
+
timed_out = true
|
|
1420
|
+
begin
|
|
1421
|
+
Process.kill("-KILL", Process.getpgid(pid))
|
|
1422
|
+
Process.wait(pid)
|
|
1423
|
+
rescue StandardError
|
|
1424
|
+
nil
|
|
1425
|
+
end
|
|
1426
|
+
end
|
|
1427
|
+
|
|
1428
|
+
output = reader.read
|
|
1429
|
+
reader.close
|
|
1430
|
+
[output, status, timed_out]
|
|
1431
|
+
end
|
|
1432
|
+
|
|
1433
|
+
def run_command
|
|
1434
|
+
path, options = extract_path_and_options
|
|
1435
|
+
return 1 unless path
|
|
1436
|
+
|
|
1437
|
+
run_and_print_result(path, options)
|
|
1438
|
+
end
|
|
1439
|
+
|
|
1440
|
+
def app_command
|
|
1441
|
+
options = parse_build_options
|
|
1442
|
+
return 1 unless options
|
|
1443
|
+
|
|
1444
|
+
module_name = @argv.shift
|
|
1445
|
+
unless module_name
|
|
1446
|
+
@err.puts("missing module name")
|
|
1447
|
+
print_usage(@err)
|
|
1448
|
+
return 1
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
path = resolve_app_module(module_name)
|
|
1452
|
+
unless path
|
|
1453
|
+
@err.puts("run-module module not found: #{module_name}")
|
|
1454
|
+
return 1
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1457
|
+
frozen = options.delete(:frozen)
|
|
1458
|
+
ensure_current_lockfile!(path) if frozen
|
|
1459
|
+
|
|
1460
|
+
run_and_print_result(path, options)
|
|
1461
|
+
end
|
|
1462
|
+
|
|
1463
|
+
def extract_path_and_options(allow_clean: false)
|
|
1464
|
+
options = parse_build_options(allow_clean:)
|
|
1465
|
+
return nil unless options
|
|
1466
|
+
|
|
1467
|
+
path = @argv.shift
|
|
1468
|
+
unless path
|
|
1469
|
+
if File.file?(File.join(Dir.pwd, "package.toml"))
|
|
1470
|
+
path = Dir.pwd
|
|
1471
|
+
else
|
|
1472
|
+
@err.puts("missing source file path")
|
|
1473
|
+
print_usage(@err)
|
|
1474
|
+
return nil
|
|
1475
|
+
end
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
[path, options]
|
|
1479
|
+
end
|
|
1480
|
+
|
|
1481
|
+
def run_and_print_result(path, options)
|
|
1482
|
+
frozen = options.delete(:frozen)
|
|
1483
|
+
ensure_current_lockfile!(path) if frozen
|
|
1484
|
+
locked = options.delete(:locked)
|
|
1485
|
+
package_graph = package_graph_for(path, locked:)
|
|
1486
|
+
preview_notice_emitted = false
|
|
1487
|
+
preview_started = lambda do |message|
|
|
1488
|
+
preview_notice_emitted = true
|
|
1489
|
+
@out.write(message)
|
|
1490
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
result = Run.run(
|
|
1494
|
+
path,
|
|
1495
|
+
module_roots: module_roots_for(path, locked:),
|
|
1496
|
+
package_graph:,
|
|
1497
|
+
frontend: @build_frontend,
|
|
1498
|
+
preview_started:,
|
|
1499
|
+
argv: @argv.dup,
|
|
1500
|
+
**options.except(:timings)
|
|
1501
|
+
)
|
|
1502
|
+
unless @out.equal?($stdout) || preview_notice_emitted
|
|
1503
|
+
@out.write(result.stdout)
|
|
1504
|
+
end
|
|
1505
|
+
@out.flush if @out.respond_to?(:flush)
|
|
1506
|
+
@err.write(result.stderr) unless @err.equal?($stderr)
|
|
1507
|
+
info("[cached]") if result.cached
|
|
1508
|
+
result.exit_status
|
|
1509
|
+
end
|
|
1510
|
+
|
|
1511
|
+
def resolve_app_module(name)
|
|
1512
|
+
relative = name.tr(".", "/").sub(%r{^/}, "") + ".mt"
|
|
1513
|
+
|
|
1514
|
+
module_roots_for(Dir.pwd).each do |root|
|
|
1515
|
+
candidate = File.join(root, "std", relative)
|
|
1516
|
+
return File.expand_path(candidate) if File.file?(candidate)
|
|
1517
|
+
|
|
1518
|
+
candidate = File.join(root, relative)
|
|
1519
|
+
return File.expand_path(candidate) if File.file?(candidate)
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
nil
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
def new_command
|
|
1526
|
+
name = @argv.shift
|
|
1527
|
+
unless name
|
|
1528
|
+
@err.puts("missing project name")
|
|
1529
|
+
print_usage(@err)
|
|
1530
|
+
return 1
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
if @argv.any?
|
|
1534
|
+
@err.puts("unknown new option #{@argv.first}")
|
|
1535
|
+
print_usage(@err)
|
|
1536
|
+
return 1
|
|
1537
|
+
end
|
|
1538
|
+
|
|
1539
|
+
result = ProjectScaffold.create(name)
|
|
1540
|
+
info("created #{result.root_path}")
|
|
1541
|
+
0
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
def deps_command
|
|
1545
|
+
PackageManagerCLI.start(
|
|
1546
|
+
@argv,
|
|
1547
|
+
out: @out,
|
|
1548
|
+
err: @err,
|
|
1549
|
+
help_printer: method(:print_deps_help),
|
|
1550
|
+
services: package_services,
|
|
1551
|
+
)
|
|
1552
|
+
end
|
|
1553
|
+
|
|
1554
|
+
def docs_command
|
|
1555
|
+
port = nil
|
|
1556
|
+
open_flag = false
|
|
1557
|
+
|
|
1558
|
+
while (arg = @argv.first)
|
|
1559
|
+
case arg
|
|
1560
|
+
when "--port", "-p"
|
|
1561
|
+
@argv.shift
|
|
1562
|
+
port = @argv.shift.to_i
|
|
1563
|
+
port = nil if port <= 0 || port > 65535
|
|
1564
|
+
when "--open", "-o"
|
|
1565
|
+
open_flag = true
|
|
1566
|
+
@argv.shift
|
|
1567
|
+
else
|
|
1568
|
+
break
|
|
1569
|
+
end
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
port = resolve_docs_port(port)
|
|
1573
|
+
|
|
1574
|
+
DocsApp.set :port, port
|
|
1575
|
+
DocsApp.set :bind, "127.0.0.1"
|
|
1576
|
+
DocsApp.set :environment, :production
|
|
1577
|
+
DocsApp.set :server, :puma
|
|
1578
|
+
|
|
1579
|
+
url = "http://127.0.0.1:#{port}/"
|
|
1580
|
+
|
|
1581
|
+
@out.puts("Serving Milk Tea docs at #{url}")
|
|
1582
|
+
@out.puts("Press Ctrl+C to stop.")
|
|
1583
|
+
|
|
1584
|
+
if open_flag
|
|
1585
|
+
open_browser(url)
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
DocsApp.run!
|
|
1589
|
+
0
|
|
1590
|
+
rescue Interrupt
|
|
1591
|
+
0
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
def resolve_docs_port(preferred)
|
|
1595
|
+
return preferred if preferred
|
|
1596
|
+
|
|
1597
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
1598
|
+
port = server.addr[1]
|
|
1599
|
+
server.close
|
|
1600
|
+
port
|
|
1601
|
+
rescue StandardError
|
|
1602
|
+
4567
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
def snapshot_command
|
|
1606
|
+
input_path = nil
|
|
1607
|
+
theme_path = nil
|
|
1608
|
+
output_path = nil
|
|
1609
|
+
|
|
1610
|
+
until @argv.empty?
|
|
1611
|
+
arg = @argv.first
|
|
1612
|
+
case arg
|
|
1613
|
+
when "--theme", "-t"
|
|
1614
|
+
@argv.shift
|
|
1615
|
+
theme_path = @argv.shift
|
|
1616
|
+
unless theme_path
|
|
1617
|
+
@err.puts("snapshot: missing value for --theme")
|
|
1618
|
+
return 1
|
|
1619
|
+
end
|
|
1620
|
+
when "--output", "-o"
|
|
1621
|
+
@argv.shift
|
|
1622
|
+
output_path = @argv.shift
|
|
1623
|
+
unless output_path
|
|
1624
|
+
@err.puts("snapshot: missing value for --output")
|
|
1625
|
+
return 1
|
|
1626
|
+
end
|
|
1627
|
+
else
|
|
1628
|
+
if arg.start_with?("-")
|
|
1629
|
+
@err.puts("snapshot: unknown option #{arg}")
|
|
1630
|
+
return 1
|
|
1631
|
+
end
|
|
1632
|
+
unless input_path
|
|
1633
|
+
input_path = @argv.shift
|
|
1634
|
+
else
|
|
1635
|
+
@err.puts("snapshot: unexpected argument #{@argv.shift}")
|
|
1636
|
+
return 1
|
|
1637
|
+
end
|
|
1638
|
+
end
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
unless input_path
|
|
1642
|
+
@err.puts("snapshot: missing source file path")
|
|
1643
|
+
print_usage(@err)
|
|
1644
|
+
return 1
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
unless File.file?(input_path)
|
|
1648
|
+
@err.puts("snapshot: source file not found: #{input_path}")
|
|
1649
|
+
return 1
|
|
1650
|
+
end
|
|
1651
|
+
|
|
1652
|
+
input_path = File.expand_path(input_path)
|
|
1653
|
+
theme_path = File.expand_path(theme_path) if theme_path
|
|
1654
|
+
output_path = File.expand_path(output_path) if output_path
|
|
1655
|
+
|
|
1656
|
+
if theme_path && !File.file?(theme_path)
|
|
1657
|
+
@err.puts("snapshot: theme file not found: #{theme_path}")
|
|
1658
|
+
return 1
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
snapshot_script = MilkTea.root.join("bindings/vscode/scripts/snapshot.js").to_s
|
|
1662
|
+
unless File.file?(snapshot_script)
|
|
1663
|
+
@err.puts("snapshot: internal script not found at #{snapshot_script}")
|
|
1664
|
+
return 1
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
args = ["node", snapshot_script, input_path]
|
|
1668
|
+
args.push("-t", theme_path) if theme_path
|
|
1669
|
+
args.push("-o", output_path) if output_path
|
|
1670
|
+
|
|
1671
|
+
semantic_result = nil
|
|
1672
|
+
begin
|
|
1673
|
+
semantic_result = MilkTea::LSP::Server.semantic_tokens_for_path(input_path)
|
|
1674
|
+
rescue => e
|
|
1675
|
+
@err.puts("snapshot: semantic analysis skipped: #{e.message}")
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
if semantic_result && semantic_result[:entries] && !semantic_result[:entries].empty?
|
|
1679
|
+
source = File.read(input_path)
|
|
1680
|
+
lines = source.split("\n", -1)
|
|
1681
|
+
semantic_result[:entries].each do |entry|
|
|
1682
|
+
byte_start = entry[:startChar]
|
|
1683
|
+
byte_len = entry[:length]
|
|
1684
|
+
line_text = lines[entry[:line]]
|
|
1685
|
+
unless line_text && byte_start && byte_len
|
|
1686
|
+
entry[:startChar] = 0
|
|
1687
|
+
entry[:length] = 0
|
|
1688
|
+
next
|
|
1689
|
+
end
|
|
1690
|
+
char_start = line_text.byteslice(0, byte_start).length
|
|
1691
|
+
char_length = line_text.byteslice(byte_start, byte_len).length
|
|
1692
|
+
char_length = line_text.length - char_start if char_start + char_length > line_text.length
|
|
1693
|
+
entry[:startChar] = char_start
|
|
1694
|
+
entry[:length] = char_length
|
|
1695
|
+
end
|
|
1696
|
+
|
|
1697
|
+
temp = Tempfile.new(["mt_semantic", ".json"])
|
|
1698
|
+
temp.write(JSON.generate(semantic_result[:entries]))
|
|
1699
|
+
temp.close
|
|
1700
|
+
args.push("-s", temp.path)
|
|
1701
|
+
system(*args)
|
|
1702
|
+
temp.unlink
|
|
1703
|
+
else
|
|
1704
|
+
system(*args)
|
|
1705
|
+
end
|
|
1706
|
+
$?.success? ? 0 : 1
|
|
1707
|
+
end
|
|
1708
|
+
|
|
1709
|
+
def lsp_command
|
|
1710
|
+
log_level = nil
|
|
1711
|
+
|
|
1712
|
+
until @argv.empty?
|
|
1713
|
+
arg = @argv.shift
|
|
1714
|
+
case arg
|
|
1715
|
+
when "--log-level"
|
|
1716
|
+
log_level = @argv.shift&.downcase
|
|
1717
|
+
unless log_level && %w[trace debug info warn error].include?(log_level)
|
|
1718
|
+
@err.puts("lsp: invalid --log-level #{log_level.inspect} (expected trace, debug, info, warn, or error)")
|
|
1719
|
+
return 1
|
|
1720
|
+
end
|
|
1721
|
+
when /\A--log-level=(.+)\z/
|
|
1722
|
+
log_level = ::Regexp.last_match(1).downcase
|
|
1723
|
+
unless %w[trace debug info warn error].include?(log_level)
|
|
1724
|
+
@err.puts("lsp: invalid --log-level #{log_level.inspect} (expected trace, debug, info, warn, or error)")
|
|
1725
|
+
return 1
|
|
1726
|
+
end
|
|
1727
|
+
when "--stdio"
|
|
1728
|
+
# stdio is the only transport; accept the flag as a no-op
|
|
1729
|
+
else
|
|
1730
|
+
if arg.start_with?("-")
|
|
1731
|
+
@err.puts("lsp: unknown option #{arg}")
|
|
1732
|
+
return 1
|
|
1733
|
+
end
|
|
1734
|
+
@err.puts("lsp: unexpected argument #{arg}")
|
|
1735
|
+
return 1
|
|
1736
|
+
end
|
|
1737
|
+
end
|
|
1738
|
+
|
|
1739
|
+
require "milk_tea/lsp/server" unless defined?(MilkTea::LSP::Server)
|
|
1740
|
+
server = MilkTea::LSP::Server.new
|
|
1741
|
+
server.run
|
|
1742
|
+
0
|
|
1743
|
+
end
|
|
1744
|
+
|
|
1745
|
+
def dap_command
|
|
1746
|
+
preferred_backend_kind = "process"
|
|
1747
|
+
adapter_command = nil
|
|
1748
|
+
|
|
1749
|
+
until @argv.empty?
|
|
1750
|
+
arg = @argv.shift
|
|
1751
|
+
case arg
|
|
1752
|
+
when "--log-level"
|
|
1753
|
+
@argv.shift
|
|
1754
|
+
when /\A--log-level=(.+)\z/
|
|
1755
|
+
# accept and ignore
|
|
1756
|
+
when "--backend"
|
|
1757
|
+
preferred_backend_kind = @argv.shift&.downcase
|
|
1758
|
+
when /\A--backend=(.+)\z/
|
|
1759
|
+
preferred_backend_kind = ::Regexp.last_match(1).downcase
|
|
1760
|
+
when "--adapter-path"
|
|
1761
|
+
adapter_path = @argv.shift
|
|
1762
|
+
adapter_command = resolve_adapter_path(adapter_path)
|
|
1763
|
+
return 1 unless adapter_command
|
|
1764
|
+
when /\A--adapter-path=(.+)\z/
|
|
1765
|
+
adapter_path = ::Regexp.last_match(1)
|
|
1766
|
+
adapter_command = resolve_adapter_path(adapter_path)
|
|
1767
|
+
return 1 unless adapter_command
|
|
1768
|
+
else
|
|
1769
|
+
if arg.start_with?("-")
|
|
1770
|
+
@err.puts("dap: unknown option #{arg}")
|
|
1771
|
+
return 1
|
|
1772
|
+
end
|
|
1773
|
+
@err.puts("dap: unexpected argument #{arg}")
|
|
1774
|
+
return 1
|
|
1775
|
+
end
|
|
1776
|
+
end
|
|
1777
|
+
|
|
1778
|
+
require "milk_tea/dap/server" unless defined?(MilkTea::DAP::Server)
|
|
1779
|
+
server = MilkTea::DAP::Server.new(
|
|
1780
|
+
preferred_backend_kind:,
|
|
1781
|
+
adapter_command:,
|
|
1782
|
+
)
|
|
1783
|
+
server.run
|
|
1784
|
+
0
|
|
1785
|
+
end
|
|
1786
|
+
|
|
1787
|
+
def resolve_adapter_path(adapter_path)
|
|
1788
|
+
unless adapter_path && File.file?(adapter_path)
|
|
1789
|
+
@err.puts("dap: adapter path not found: #{adapter_path}")
|
|
1790
|
+
return nil
|
|
1791
|
+
end
|
|
1792
|
+
expanded = File.expand_path(adapter_path)
|
|
1793
|
+
adapter_path.end_with?(".rb") ? [RbConfig.ruby, expanded] : [expanded]
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def toolchain_command
|
|
1797
|
+
ToolchainCLI.start(
|
|
1798
|
+
@argv,
|
|
1799
|
+
out: @out,
|
|
1800
|
+
err: @err,
|
|
1801
|
+
help_printer: method(:print_toolchain_help),
|
|
1802
|
+
)
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
def debug_command
|
|
1806
|
+
unless @argv.any?
|
|
1807
|
+
@err.puts("missing source file path")
|
|
1808
|
+
print_usage(@err)
|
|
1809
|
+
return 1
|
|
1810
|
+
end
|
|
1811
|
+
|
|
1812
|
+
resolution = extract_resolution_flags!
|
|
1813
|
+
input_paths = @argv.dup
|
|
1814
|
+
return 1 unless ensure_known_source_operands!("debug", input_paths)
|
|
1815
|
+
|
|
1816
|
+
path = expand_source_paths(input_paths).first
|
|
1817
|
+
unless path
|
|
1818
|
+
@err.puts("no .mt files found in #{input_paths.join(', ')}")
|
|
1819
|
+
return 1
|
|
1820
|
+
end
|
|
1821
|
+
|
|
1822
|
+
ensure_current_lockfile!(path) if resolution[:frozen]
|
|
1823
|
+
|
|
1824
|
+
source = read_source_file(path)
|
|
1825
|
+
resolved_path = File.expand_path(path)
|
|
1826
|
+
|
|
1827
|
+
tokens = MilkTea::Lexer.lex(source, path: resolved_path)
|
|
1828
|
+
|
|
1829
|
+
parse_result = MilkTea::Parser.parse_collecting_errors(source, path: resolved_path)
|
|
1830
|
+
ast = parse_result.ast
|
|
1831
|
+
parse_errors = parse_result.errors.dup
|
|
1832
|
+
|
|
1833
|
+
facts = nil
|
|
1834
|
+
snapshot = nil
|
|
1835
|
+
loader_ast = ast
|
|
1836
|
+
|
|
1837
|
+
if ast && parse_errors.empty?
|
|
1838
|
+
begin
|
|
1839
|
+
loader = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform)
|
|
1840
|
+
loader_ast = loader.load_file(resolved_path)
|
|
1841
|
+
|
|
1842
|
+
import_result = loader.send(:imported_modules_for_ast_collecting_errors, loader_ast, importer_path: resolved_path)
|
|
1843
|
+
import_errors = import_result.respond_to?(:errors) ? import_result.errors : []
|
|
1844
|
+
parse_errors.concat(import_errors) unless import_errors.empty?
|
|
1845
|
+
|
|
1846
|
+
snapshot = MilkTea::SemanticAnalyzer.tooling_snapshot(
|
|
1847
|
+
loader_ast,
|
|
1848
|
+
imported_modules: import_result.modules,
|
|
1849
|
+
allow_missing_imports: true,
|
|
1850
|
+
path: resolved_path,
|
|
1851
|
+
)
|
|
1852
|
+
facts = snapshot&.facts
|
|
1853
|
+
rescue MilkTea::LexError, MilkTea::ParseError, ModuleLoadError, SemanticError => e
|
|
1854
|
+
parse_errors << e
|
|
1855
|
+
end
|
|
1856
|
+
end
|
|
1857
|
+
|
|
1858
|
+
text = DebugInfoFormatter.format_all(
|
|
1859
|
+
content: source,
|
|
1860
|
+
tokens: tokens,
|
|
1861
|
+
ast: loader_ast,
|
|
1862
|
+
parse_errors: parse_errors,
|
|
1863
|
+
facts: facts,
|
|
1864
|
+
snapshot: snapshot,
|
|
1865
|
+
path: resolved_path,
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
@out.puts(text)
|
|
1869
|
+
0
|
|
1870
|
+
rescue MilkTea::LexError => e
|
|
1871
|
+
@err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
|
|
1872
|
+
1
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
def bindgen_command
|
|
1876
|
+
BindgenCLI.start(@argv, out: @out, err: @err, help_printer: method(:print_bindgen_help))
|
|
1877
|
+
end
|
|
1878
|
+
|
|
1879
|
+
def cache_command
|
|
1880
|
+
subcommand = @argv.shift
|
|
1881
|
+
unless subcommand
|
|
1882
|
+
@err.puts("missing cache subcommand")
|
|
1883
|
+
print_command_help("cache", @err)
|
|
1884
|
+
return 1
|
|
1885
|
+
end
|
|
1886
|
+
|
|
1887
|
+
cache_root = MilkTea.data_root.join("tmp", "mtc-cache")
|
|
1888
|
+
|
|
1889
|
+
case subcommand
|
|
1890
|
+
when "purge"
|
|
1891
|
+
if File.directory?(cache_root)
|
|
1892
|
+
FileUtils.rm_rf(cache_root)
|
|
1893
|
+
@out.puts("purged #{cache_root}")
|
|
1894
|
+
else
|
|
1895
|
+
@out.puts("cache is already empty")
|
|
1896
|
+
end
|
|
1897
|
+
0
|
|
1898
|
+
when "status"
|
|
1899
|
+
unless File.directory?(cache_root)
|
|
1900
|
+
@out.puts("cache directory does not exist: #{cache_root}")
|
|
1901
|
+
return 0
|
|
1902
|
+
end
|
|
1903
|
+
program_dirs = Dir.glob(File.join(cache_root, "programs", "*", "*")).select { |d| File.directory?(d) }
|
|
1904
|
+
binary_files = Dir.glob(File.join(cache_root, "binaries", "*", "*", "binary")).select { |f| File.file?(f) }
|
|
1905
|
+
total_size = (program_dirs + binary_files).sum { |p|
|
|
1906
|
+
File.file?(p) ? File.size(p) : Dir.glob(File.join(p, "**", "*")).sum { |f| File.file?(f) ? File.size(f) : 0 }
|
|
1907
|
+
}
|
|
1908
|
+
@out.puts("cache #{program_dirs.length} programs, #{binary_files.length} binaries (#{format_size(total_size)})")
|
|
1909
|
+
@out.puts(" root #{cache_root}")
|
|
1910
|
+
0
|
|
1911
|
+
else
|
|
1912
|
+
@err.puts("unknown cache subcommand #{subcommand}")
|
|
1913
|
+
print_command_help("cache", @err)
|
|
1914
|
+
1
|
|
1915
|
+
end
|
|
1916
|
+
end
|
|
1917
|
+
|
|
1918
|
+
def completions_command
|
|
1919
|
+
shell = @argv.shift
|
|
1920
|
+
unless %w[bash zsh fish].include?(shell)
|
|
1921
|
+
@err.puts("completions: shell must be bash, zsh, or fish")
|
|
1922
|
+
print_command_help("completions", @err)
|
|
1923
|
+
return 1
|
|
1924
|
+
end
|
|
1925
|
+
|
|
1926
|
+
@out.puts(completion_script(shell))
|
|
1927
|
+
0
|
|
1928
|
+
end
|
|
1929
|
+
|
|
1930
|
+
def completion_script(shell)
|
|
1931
|
+
names = COMMANDS.map(&:first)
|
|
1932
|
+
case shell
|
|
1933
|
+
when "bash"
|
|
1934
|
+
[
|
|
1935
|
+
"# mtc bash completion. Source this file or install it into your bash",
|
|
1936
|
+
"# completion directory (e.g. /etc/bash_completion.d/mtc).",
|
|
1937
|
+
"_mtc() {",
|
|
1938
|
+
%( local cur="${COMP_WORDS[COMP_CWORD]}"),
|
|
1939
|
+
%( if [ "${COMP_CWORD}" -eq 1 ]; then),
|
|
1940
|
+
%( COMPREPLY=( $(compgen -W "#{(names + %w[help version]).join(' ')}" -- "${cur}") )),
|
|
1941
|
+
" fi",
|
|
1942
|
+
"}",
|
|
1943
|
+
"complete -F _mtc mtc",
|
|
1944
|
+
].join("\n")
|
|
1945
|
+
when "zsh"
|
|
1946
|
+
lines = ["#compdef mtc", "# mtc zsh completion. Install onto your $fpath as _mtc.", "_mtc() {", " local -a commands", " commands=("]
|
|
1947
|
+
COMMANDS.each { |name, summary| lines << " '#{name}:#{summary}'" }
|
|
1948
|
+
lines.concat([" )", " if (( CURRENT == 2 )); then", " _describe 'mtc command' commands", " fi", "}", %(_mtc "$@")])
|
|
1949
|
+
lines.join("\n")
|
|
1950
|
+
when "fish"
|
|
1951
|
+
lines = ["# mtc fish completion. Install into ~/.config/fish/completions/mtc.fish."]
|
|
1952
|
+
COMMANDS.each do |name, summary|
|
|
1953
|
+
lines << "complete -c mtc -f -n '__fish_use_subcommand' -a #{name} -d '#{summary}'"
|
|
1954
|
+
end
|
|
1955
|
+
lines.join("\n")
|
|
1956
|
+
end
|
|
1957
|
+
end
|
|
1958
|
+
|
|
1959
|
+
def parse_build_options(allow_clean: false)
|
|
1960
|
+
options = {
|
|
1961
|
+
output_path: nil,
|
|
1962
|
+
cc: ENV.fetch("CC", "cc"),
|
|
1963
|
+
keep_c_path: nil,
|
|
1964
|
+
profile: nil,
|
|
1965
|
+
platform: nil,
|
|
1966
|
+
bundle: false,
|
|
1967
|
+
archive: false,
|
|
1968
|
+
locked: false,
|
|
1969
|
+
frozen: false,
|
|
1970
|
+
no_cache: false,
|
|
1971
|
+
kind: :executable,
|
|
1972
|
+
timings: false,
|
|
1973
|
+
debug_guards: nil,
|
|
1974
|
+
}
|
|
1975
|
+
options[:clean] = false if allow_clean
|
|
1976
|
+
|
|
1977
|
+
positional = []
|
|
1978
|
+
until @argv.empty?
|
|
1979
|
+
option = @argv.shift
|
|
1980
|
+
case option
|
|
1981
|
+
when "-o", "--output"
|
|
1982
|
+
value = @argv.shift
|
|
1983
|
+
return missing_option_value(option) unless value
|
|
1984
|
+
|
|
1985
|
+
options[:output_path] = value
|
|
1986
|
+
when "--cc"
|
|
1987
|
+
value = @argv.shift
|
|
1988
|
+
return missing_option_value(option) unless value
|
|
1989
|
+
|
|
1990
|
+
options[:cc] = value
|
|
1991
|
+
when "--keep-c"
|
|
1992
|
+
value = @argv.shift
|
|
1993
|
+
return missing_option_value(option) unless value
|
|
1994
|
+
|
|
1995
|
+
options[:keep_c_path] = value
|
|
1996
|
+
when "--profile"
|
|
1997
|
+
value = @argv.shift
|
|
1998
|
+
return missing_option_value(option) unless value
|
|
1999
|
+
|
|
2000
|
+
options[:profile] = value
|
|
2001
|
+
when "--platform"
|
|
2002
|
+
value = @argv.shift
|
|
2003
|
+
return missing_option_value(option) unless value
|
|
2004
|
+
|
|
2005
|
+
options[:platform] = value
|
|
2006
|
+
when "--bundle"
|
|
2007
|
+
options[:bundle] = true
|
|
2008
|
+
when "--archive"
|
|
2009
|
+
options[:bundle] = true
|
|
2010
|
+
options[:archive] = true
|
|
2011
|
+
when "--locked"
|
|
2012
|
+
options[:locked] = true
|
|
2013
|
+
when "--frozen"
|
|
2014
|
+
options[:locked] = true
|
|
2015
|
+
options[:frozen] = true
|
|
2016
|
+
when "--no-cache"
|
|
2017
|
+
options[:no_cache] = true
|
|
2018
|
+
when "--debug-guards"
|
|
2019
|
+
options[:debug_guards] = true
|
|
2020
|
+
when "--no-debug-guards"
|
|
2021
|
+
options[:debug_guards] = false
|
|
2022
|
+
when "--timings"
|
|
2023
|
+
options[:timings] = true
|
|
2024
|
+
when "--kind"
|
|
2025
|
+
value = @argv.shift
|
|
2026
|
+
return missing_option_value(option) unless value
|
|
2027
|
+
|
|
2028
|
+
options[:kind] = case value
|
|
2029
|
+
when "executable", "exe", "bin" then :executable
|
|
2030
|
+
when "static", "staticlib" then :static
|
|
2031
|
+
when "shared", "dylib", "dll", "so" then :shared
|
|
2032
|
+
else
|
|
2033
|
+
@err.puts("unknown build kind #{value}; expected executable|static|shared")
|
|
2034
|
+
print_usage(@err)
|
|
2035
|
+
return nil
|
|
2036
|
+
end
|
|
2037
|
+
when "--clean"
|
|
2038
|
+
if allow_clean
|
|
2039
|
+
options[:clean] = true
|
|
2040
|
+
else
|
|
2041
|
+
@err.puts("unknown build option #{option}")
|
|
2042
|
+
print_usage(@err)
|
|
2043
|
+
return nil
|
|
2044
|
+
end
|
|
2045
|
+
when "--"
|
|
2046
|
+
positional.concat(@argv)
|
|
2047
|
+
@argv.clear
|
|
2048
|
+
else
|
|
2049
|
+
if option.start_with?("-")
|
|
2050
|
+
@err.puts("unknown build option #{option}")
|
|
2051
|
+
print_usage(@err)
|
|
2052
|
+
return nil
|
|
2053
|
+
end
|
|
2054
|
+
positional << option
|
|
2055
|
+
end
|
|
2056
|
+
end
|
|
2057
|
+
|
|
2058
|
+
@argv = positional
|
|
2059
|
+
options
|
|
2060
|
+
end
|
|
2061
|
+
|
|
2062
|
+
def parse_format_options
|
|
2063
|
+
options = {
|
|
2064
|
+
check: false,
|
|
2065
|
+
write: false,
|
|
2066
|
+
mode: :safe,
|
|
2067
|
+
max_line_length: nil,
|
|
2068
|
+
profile: false,
|
|
2069
|
+
}
|
|
2070
|
+
input_paths = []
|
|
2071
|
+
|
|
2072
|
+
until @argv.empty?
|
|
2073
|
+
option = @argv.shift
|
|
2074
|
+
if option.start_with?("-")
|
|
2075
|
+
case option
|
|
2076
|
+
when "--check"
|
|
2077
|
+
options[:check] = true
|
|
2078
|
+
when "--write", "-w"
|
|
2079
|
+
options[:write] = true
|
|
2080
|
+
when "--preserve"
|
|
2081
|
+
options[:mode] = :preserve
|
|
2082
|
+
when "--canonical"
|
|
2083
|
+
options[:mode] = :canonical
|
|
2084
|
+
when "--safe"
|
|
2085
|
+
options[:mode] = :safe
|
|
2086
|
+
when "--tidy"
|
|
2087
|
+
options[:mode] = :tidy
|
|
2088
|
+
when "--max-line-length"
|
|
2089
|
+
value = @argv.shift
|
|
2090
|
+
return missing_option_value(option) unless value
|
|
2091
|
+
|
|
2092
|
+
line_length = Integer(value, exception: false)
|
|
2093
|
+
unless line_length && line_length.positive?
|
|
2094
|
+
@err.puts("--max-line-length must be a positive integer")
|
|
2095
|
+
print_usage(@err)
|
|
2096
|
+
return nil
|
|
2097
|
+
end
|
|
2098
|
+
|
|
2099
|
+
options[:max_line_length] = line_length
|
|
2100
|
+
when "--timings"
|
|
2101
|
+
options[:profile] = true
|
|
2102
|
+
when "--"
|
|
2103
|
+
input_paths.concat(@argv)
|
|
2104
|
+
@argv.clear
|
|
2105
|
+
else
|
|
2106
|
+
@err.puts("unknown format option #{option}")
|
|
2107
|
+
print_usage(@err)
|
|
2108
|
+
return nil
|
|
2109
|
+
end
|
|
2110
|
+
else
|
|
2111
|
+
input_paths << option
|
|
2112
|
+
end
|
|
2113
|
+
end
|
|
2114
|
+
|
|
2115
|
+
if options[:check] && options[:write]
|
|
2116
|
+
@err.puts("format options --check and --write cannot be combined")
|
|
2117
|
+
print_usage(@err)
|
|
2118
|
+
return nil
|
|
2119
|
+
end
|
|
2120
|
+
|
|
2121
|
+
{ options:, input_paths: }
|
|
2122
|
+
end
|
|
2123
|
+
|
|
2124
|
+
def ensure_known_source_operands!(command, operands)
|
|
2125
|
+
return true unless operands.any? { |arg| arg.start_with?("-") }
|
|
2126
|
+
|
|
2127
|
+
@err.puts("unexpected argument(s) for #{command}: #{operands.join(' ')}")
|
|
2128
|
+
print_command_help(command, @err)
|
|
2129
|
+
false
|
|
2130
|
+
end
|
|
2131
|
+
|
|
2132
|
+
def expand_source_paths(input_paths)
|
|
2133
|
+
input_paths.flat_map do |path|
|
|
2134
|
+
if File.directory?(path)
|
|
2135
|
+
Dir.glob(File.join(path, "**/*.mt")).sort
|
|
2136
|
+
else
|
|
2137
|
+
[path]
|
|
2138
|
+
end
|
|
2139
|
+
end.uniq
|
|
2140
|
+
end
|
|
2141
|
+
|
|
2142
|
+
def print_no_source_files_if_empty(paths, input_paths)
|
|
2143
|
+
return false unless paths.empty?
|
|
2144
|
+
|
|
2145
|
+
label = input_paths.length == 1 ? input_paths.first : input_paths.join(", ")
|
|
2146
|
+
@out.puts("no .mt files found in #{label}")
|
|
2147
|
+
true
|
|
2148
|
+
end
|
|
2149
|
+
|
|
2150
|
+
def missing_option_value(option)
|
|
2151
|
+
@err.puts("missing value for #{option}")
|
|
2152
|
+
print_usage(@err)
|
|
2153
|
+
nil
|
|
2154
|
+
end
|
|
2155
|
+
|
|
2156
|
+
def handled_cli_error?(error)
|
|
2157
|
+
handled_error_classes.any? { |klass| error.is_a?(klass) }
|
|
2158
|
+
end
|
|
2159
|
+
|
|
2160
|
+
def handled_error_classes
|
|
2161
|
+
classes = [LexError, ParseError, ModuleLoadError, SemanticError, LoweringError, CBackendError, BuildError, RunError, FormatterError, PackageManifestError, PackageManifestEditorError, PackageGraphError, PackageLockError, PackageSourceResolverError, PackageSourceFetcherError, PackageRegistryStoreError, PackageRegistryMetadataProviderError, PackageDependencySolverError, PackageVersionError, ProjectScaffoldError]
|
|
2162
|
+
classes << BindgenError if MilkTea.const_defined?(:BindgenError, false)
|
|
2163
|
+
classes << UpstreamSources::Error if MilkTea.const_defined?(:UpstreamSources, false)
|
|
2164
|
+
classes
|
|
2165
|
+
end
|
|
2166
|
+
|
|
2167
|
+
def read_source_file(path)
|
|
2168
|
+
resolved_path = File.expand_path(path.to_s)
|
|
2169
|
+
@source_overrides.fetch(resolved_path) { File.read(resolved_path) }
|
|
2170
|
+
rescue Errno::ENOENT
|
|
2171
|
+
raise ModuleLoadError.new("source file not found", path: path)
|
|
2172
|
+
rescue Errno::EISDIR
|
|
2173
|
+
raise ModuleLoadError.new("expected a source file, got a directory", path: path)
|
|
2174
|
+
end
|
|
2175
|
+
|
|
2176
|
+
def generated_source?(source)
|
|
2177
|
+
source.start_with?(GENERATED_FILE_HEADER_PREFIX)
|
|
2178
|
+
end
|
|
2179
|
+
|
|
2180
|
+
ACTION_LABELS = {
|
|
2181
|
+
"format-check" => "checking",
|
|
2182
|
+
"format-write" => "formatting",
|
|
2183
|
+
"lint" => "linting",
|
|
2184
|
+
"lint-fix" => "fixing",
|
|
2185
|
+
}.freeze
|
|
2186
|
+
|
|
2187
|
+
def announce_file_action(path, action)
|
|
2188
|
+
return unless @verbose
|
|
2189
|
+
|
|
2190
|
+
@out.puts("#{ACTION_LABELS.fetch(action, action)} #{path}")
|
|
2191
|
+
end
|
|
2192
|
+
|
|
2193
|
+
def make_module_loader(path = nil, locked: false, platform: nil)
|
|
2194
|
+
ModuleLoader.new(module_roots: module_roots_for(path, locked:), package_graph: package_graph_for(path, locked:), source_overrides: @source_overrides, platform:)
|
|
2195
|
+
end
|
|
2196
|
+
|
|
2197
|
+
def normalize_source_overrides(source_overrides)
|
|
2198
|
+
return {} unless source_overrides
|
|
2199
|
+
|
|
2200
|
+
source_overrides.each_with_object({}) do |(path, source), overrides|
|
|
2201
|
+
overrides[File.expand_path(path.to_s)] = source.to_s
|
|
2202
|
+
end
|
|
2203
|
+
end
|
|
2204
|
+
|
|
2205
|
+
def module_roots_for(path = nil, locked: false)
|
|
2206
|
+
roots = @include_module_roots.dup
|
|
2207
|
+
|
|
2208
|
+
if path
|
|
2209
|
+
MilkTea::ModuleRoots.roots_for_path(path, locked:).each do |root|
|
|
2210
|
+
roots << root unless roots.include?(root)
|
|
2211
|
+
end
|
|
2212
|
+
end
|
|
2213
|
+
|
|
2214
|
+
@ambient_module_roots.each do |root|
|
|
2215
|
+
roots << root unless roots.include?(root)
|
|
2216
|
+
end
|
|
2217
|
+
roots
|
|
2218
|
+
end
|
|
2219
|
+
|
|
2220
|
+
def package_graph_for(path = nil, locked: false)
|
|
2221
|
+
return nil unless path
|
|
2222
|
+
|
|
2223
|
+
PackageGraph.load(path, locked:)
|
|
2224
|
+
rescue PackageManifestError
|
|
2225
|
+
nil
|
|
2226
|
+
end
|
|
2227
|
+
|
|
2228
|
+
def package_services
|
|
2229
|
+
@package_services ||= PackageServices.new
|
|
2230
|
+
end
|
|
2231
|
+
|
|
2232
|
+
def extract_resolution_flags!
|
|
2233
|
+
locked = false
|
|
2234
|
+
frozen = false
|
|
2235
|
+
warnings_as_errors = false
|
|
2236
|
+
remaining = []
|
|
2237
|
+
i = 0
|
|
2238
|
+
while i < @argv.length
|
|
2239
|
+
arg = @argv[i]
|
|
2240
|
+
if arg == "--locked"
|
|
2241
|
+
locked = true
|
|
2242
|
+
elsif arg == "--frozen"
|
|
2243
|
+
locked = true
|
|
2244
|
+
frozen = true
|
|
2245
|
+
elsif arg == "-Werror" || arg == "--warnings-as-errors"
|
|
2246
|
+
warnings_as_errors = true
|
|
2247
|
+
elsif arg == "--"
|
|
2248
|
+
remaining.concat(@argv[i + 1..])
|
|
2249
|
+
break
|
|
2250
|
+
else
|
|
2251
|
+
remaining << arg
|
|
2252
|
+
end
|
|
2253
|
+
i += 1
|
|
2254
|
+
end
|
|
2255
|
+
@argv = remaining
|
|
2256
|
+
{ locked:, frozen:, warnings_as_errors: }
|
|
2257
|
+
end
|
|
2258
|
+
|
|
2259
|
+
def ensure_no_extra_arguments!(command)
|
|
2260
|
+
return true if @argv.empty?
|
|
2261
|
+
|
|
2262
|
+
@err.puts("unexpected argument(s) for #{command}: #{@argv.join(' ')}")
|
|
2263
|
+
print_command_help(command, @err)
|
|
2264
|
+
false
|
|
2265
|
+
end
|
|
2266
|
+
|
|
2267
|
+
def ensure_current_lockfile!(path)
|
|
2268
|
+
result = PackageLock.check(path, source_resolver: package_services.source_resolver(:cache))
|
|
2269
|
+
return if result.current?
|
|
2270
|
+
|
|
2271
|
+
message = if result.missing?
|
|
2272
|
+
"package.lock is missing: #{result.lock_path}"
|
|
2273
|
+
else
|
|
2274
|
+
"package.lock is out of date: #{result.lock_path}"
|
|
2275
|
+
end
|
|
2276
|
+
raise PackageLockError, message
|
|
2277
|
+
end
|
|
2278
|
+
|
|
2279
|
+
def ensure_current_lockfiles!(paths)
|
|
2280
|
+
checked = {}
|
|
2281
|
+
|
|
2282
|
+
paths.each do |path|
|
|
2283
|
+
result = PackageLock.check(path, source_resolver: package_services.source_resolver(:cache))
|
|
2284
|
+
next if checked[result.lock_path]
|
|
2285
|
+
|
|
2286
|
+
checked[result.lock_path] = true
|
|
2287
|
+
next if result.current?
|
|
2288
|
+
|
|
2289
|
+
message = if result.missing?
|
|
2290
|
+
"package.lock is missing: #{result.lock_path}"
|
|
2291
|
+
else
|
|
2292
|
+
"package.lock is out of date: #{result.lock_path}"
|
|
2293
|
+
end
|
|
2294
|
+
raise PackageLockError, message
|
|
2295
|
+
end
|
|
2296
|
+
end
|
|
2297
|
+
|
|
2298
|
+
def lint_sema_facts_for(source, path, locked: false)
|
|
2299
|
+
ast = Parser.parse(source, path: path)
|
|
2300
|
+
imported_modules = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform).imported_modules_for_ast(ast, importer_path: path)
|
|
2301
|
+
SemanticAnalyzer.tooling_snapshot(ast, imported_modules: imported_modules, path: path).facts
|
|
2302
|
+
rescue MilkTea::LexError, MilkTea::ParseError, SemanticError, ModuleLoadError
|
|
2303
|
+
nil
|
|
2304
|
+
end
|
|
2305
|
+
|
|
2306
|
+
def extract_include_paths!
|
|
2307
|
+
remaining = []
|
|
2308
|
+
i = 0
|
|
2309
|
+
while i < @argv.length
|
|
2310
|
+
if @argv[i] == "-I" || @argv[i] == "--include-path"
|
|
2311
|
+
value = @argv[i + 1]
|
|
2312
|
+
if value && !value.start_with?("-")
|
|
2313
|
+
@include_path_flags << @argv[i]
|
|
2314
|
+
@include_module_roots << File.expand_path(value)
|
|
2315
|
+
i += 2
|
|
2316
|
+
else
|
|
2317
|
+
@err.puts("missing value for #{@argv[i]}")
|
|
2318
|
+
i += 1
|
|
2319
|
+
end
|
|
2320
|
+
else
|
|
2321
|
+
remaining << @argv[i]
|
|
2322
|
+
i += 1
|
|
2323
|
+
end
|
|
2324
|
+
end
|
|
2325
|
+
@argv = remaining
|
|
2326
|
+
end
|
|
2327
|
+
|
|
2328
|
+
def open_browser(url)
|
|
2329
|
+
command = host_platform == :windows ? ["cmd", "/c", "start", "", url] : ["xdg-open", url]
|
|
2330
|
+
pid = Process.spawn(*command, out: File::NULL, err: File::NULL)
|
|
2331
|
+
Process.detach(pid)
|
|
2332
|
+
rescue Errno::ENOENT
|
|
2333
|
+
@err.puts("note: could not open browser (#{command.first} not found)")
|
|
2334
|
+
end
|
|
2335
|
+
|
|
2336
|
+
def host_platform
|
|
2337
|
+
MilkTea.host_platform
|
|
2338
|
+
end
|
|
2339
|
+
|
|
2340
|
+
def command_supports_include_paths?(command)
|
|
2341
|
+
%w[parse lint check lower emit-c build run test debug].include?(command)
|
|
2342
|
+
end
|
|
2343
|
+
|
|
2344
|
+
COMMAND_HELP = {
|
|
2345
|
+
"test" => <<~HELP,
|
|
2346
|
+
Usage: mtc test PATH|DIR [--cc COMPILER] [--profile debug|release] [--platform linux|windows|wasm] [--timeout SECONDS] [--mem MB] [--jobs N] [--sanitize] [-n SUBSTRING] [--format human|tap|junit] [--locked] [--frozen] [-I PATH]
|
|
2347
|
+
|
|
2348
|
+
Discover and run @[test] functions in a Milk Tea source file or directory.
|
|
2349
|
+
|
|
2350
|
+
Given a file, runs its @[test] functions. Given a directory (or a package
|
|
2351
|
+
root), recursively discovers every .mt file that contains @[test] functions,
|
|
2352
|
+
runs each as its own test binary, and prints an aggregate summary.
|
|
2353
|
+
|
|
2354
|
+
Functions annotated with @[test] must take no parameters and return
|
|
2355
|
+
std.testing.Check. `mtc test` synthesizes a runner that invokes each
|
|
2356
|
+
test through std.testing and reports the results; a test file must import
|
|
2357
|
+
std.testing and must not define `main`.
|
|
2358
|
+
|
|
2359
|
+
Each test binary runs under a wall-clock timeout and an address-space
|
|
2360
|
+
memory cap so a hanging or runaway test cannot stall or exhaust the host.
|
|
2361
|
+
Override them with --timeout SECONDS (default 30) and --mem MB (default 1024).
|
|
2362
|
+
For a directory, --jobs N builds and runs N files in parallel (default 1; output
|
|
2363
|
+
stays in file order).
|
|
2364
|
+
A file containing `# expect-error: <text>` is a compile-fail fixture: `mtc test`
|
|
2365
|
+
requires the compiler to reject it with a diagnostic containing that text.
|
|
2366
|
+
--sanitize builds test binaries with AddressSanitizer/UBSan (including leak
|
|
2367
|
+
detection); any sanitizer error fails the run.
|
|
2368
|
+
-n SUBSTRING runs only @[test] functions whose name contains SUBSTRING (and, in a
|
|
2369
|
+
directory, compile-fail fixtures whose filename matches).
|
|
2370
|
+
--format tap|junit emits machine-readable results (TAP / JUnit XML) instead of the
|
|
2371
|
+
human summary, for CI.
|
|
2372
|
+
HELP
|
|
2373
|
+
"debug" => "Usage: mtc debug PATH [--locked] [--frozen] [-I PATH]\n\n Print debug information for a source file: tokens, AST, semantic facts,\n binding resolution, and diagnostics.",
|
|
2374
|
+
"lex" => "Usage: mtc lex PATH\n\n Tokenize a source file and print the token stream.",
|
|
2375
|
+
"parse" => "Usage: mtc parse PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-I PATH]\n\n Parse one or more source files and print the AST.\n\n Options:\n --locked Resolve dependencies from package.lock.\n --frozen Require a current package.lock.\n -I, --include-path Add an extra module root.",
|
|
2376
|
+
"format" => <<~HELP,
|
|
2377
|
+
Usage: mtc format PATH|DIR [PATH|DIR ...] [OPTIONS]
|
|
2378
|
+
|
|
2379
|
+
Format Milk Tea source files.
|
|
2380
|
+
|
|
2381
|
+
Options:
|
|
2382
|
+
--check Report files that need formatting without writing them.
|
|
2383
|
+
--write, -w Rewrite files in place.
|
|
2384
|
+
--safe Format only unambiguous style changes (default).
|
|
2385
|
+
--canonical Apply all canonical style normalisations.
|
|
2386
|
+
--preserve Preserve existing formatting where possible.
|
|
2387
|
+
--tidy Apply tidy formatting with line wrapping and blank-line normalization.
|
|
2388
|
+
--max-line-length N
|
|
2389
|
+
Override the line length used by tidy mode.
|
|
2390
|
+
--timings Print per-file format timing breakdown.
|
|
2391
|
+
|
|
2392
|
+
When formatting directories or multiple files, --check or --write is required.
|
|
2393
|
+
HELP
|
|
2394
|
+
"lint" => <<~HELP,
|
|
2395
|
+
Usage: mtc lint PATH|DIR [OPTIONS]
|
|
2396
|
+
mtc lint --init
|
|
2397
|
+
|
|
2398
|
+
Lint Milk Tea source files and report warnings.
|
|
2399
|
+
Some rules and auto-fixes are semantic/import-aware, so lint can use
|
|
2400
|
+
the same dependency resolution mode as check/build.
|
|
2401
|
+
|
|
2402
|
+
Options:
|
|
2403
|
+
--init Create a default .mt-lint.yml in the current directory.
|
|
2404
|
+
--select RULES Comma-separated list of rule codes to enable.
|
|
2405
|
+
--ignore RULES Comma-separated list of rule codes to suppress.
|
|
2406
|
+
--fix Apply auto-fixable changes in place.
|
|
2407
|
+
--ignore-generated Skip files that start with '# generated by mtc'.
|
|
2408
|
+
--timings Print lint timing: per-file summary and per-rule breakdown.
|
|
2409
|
+
--locked Use package.lock for semantic dependency resolution.
|
|
2410
|
+
--frozen Require a current package.lock before semantic dependency resolution.
|
|
2411
|
+
-I, --include-path PATH Add an extra module root for semantic resolution.
|
|
2412
|
+
HELP
|
|
2413
|
+
"check" => <<~HELP,
|
|
2414
|
+
Usage: mtc check PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-Werror] [-I PATH]
|
|
2415
|
+
|
|
2416
|
+
Run semantic analysis on one or more source files and report errors.
|
|
2417
|
+
|
|
2418
|
+
Options:
|
|
2419
|
+
-Werror Treat warnings as errors.
|
|
2420
|
+
--locked / --frozen Dependency resolution mode.
|
|
2421
|
+
-I, --include-path PATH Add an extra module root.
|
|
2422
|
+
HELP
|
|
2423
|
+
"lower" => "Usage: mtc lower PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-I PATH]\n\n Lower one or more source files to IR and print it.",
|
|
2424
|
+
"emit-c" => "Usage: mtc emit-c PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-I PATH]\n\n Compile one or more source files to C and print the output.",
|
|
2425
|
+
"build" => <<~HELP,
|
|
2426
|
+
Usage: mtc build [PATH_OR_PACKAGE] [OPTIONS]
|
|
2427
|
+
|
|
2428
|
+
Compile a source file or package. Defaults to current directory if a
|
|
2429
|
+
package.toml is present.
|
|
2430
|
+
|
|
2431
|
+
Options:
|
|
2432
|
+
-o, --output OUTPUT Output path for the compiled artifact.
|
|
2433
|
+
--cc COMPILER C compiler to use (default: $CC or cc).
|
|
2434
|
+
--keep-c C_PATH Write the generated C source to this path.
|
|
2435
|
+
--profile PROFILE debug (default) | release.
|
|
2436
|
+
--platform PLATFORM linux (default) | windows | wasm.
|
|
2437
|
+
--bundle Package a native package build into a distributable directory.
|
|
2438
|
+
--archive Also write a .tar.gz archive for the native bundle (implies --bundle).
|
|
2439
|
+
--locked Resolve dependencies from package.lock.
|
|
2440
|
+
--frozen Require a current package.lock and use locked resolution.
|
|
2441
|
+
--no-cache Skip build cache, force rebuild from source.
|
|
2442
|
+
--debug-guards Enable loop iteration and recursion guards (default: on in debug, off in release).
|
|
2443
|
+
--no-debug-guards Disable loop iteration and recursion guards.
|
|
2444
|
+
--clean Remove existing build outputs and exit.
|
|
2445
|
+
-I, --include-path PATH Add an extra module root.
|
|
2446
|
+
HELP
|
|
2447
|
+
"new" => <<~HELP,
|
|
2448
|
+
Usage: mtc new NAME
|
|
2449
|
+
|
|
2450
|
+
Create a new application package scaffold with package.toml and src/main.mt.
|
|
2451
|
+
NAME selects the target directory, and its basename is normalized to
|
|
2452
|
+
snake_case for package.name and the generated module declaration.
|
|
2453
|
+
The target may be a new directory or an existing empty directory.
|
|
2454
|
+
HELP
|
|
2455
|
+
"run" => <<~HELP,
|
|
2456
|
+
Usage: mtc run [PATH_OR_PACKAGE] [OPTIONS]
|
|
2457
|
+
|
|
2458
|
+
Build and execute an executable target. For wasm targets this starts a
|
|
2459
|
+
local preview server, opens the generated HTML in your browser, and
|
|
2460
|
+
keeps serving until you press Ctrl-C.
|
|
2461
|
+
Defaults to current directory if a package.toml is present.
|
|
2462
|
+
|
|
2463
|
+
Options:
|
|
2464
|
+
-o, --output OUTPUT Output path for the compiled binary.
|
|
2465
|
+
--cc COMPILER C compiler to use (default: $CC or cc).
|
|
2466
|
+
--keep-c C_PATH Write the generated C source to this path.
|
|
2467
|
+
--profile PROFILE debug (default) | release.
|
|
2468
|
+
--platform PLATFORM linux (default) | windows | wasm.
|
|
2469
|
+
--locked Resolve dependencies from package.lock.
|
|
2470
|
+
--frozen Require a current package.lock and use locked resolution.
|
|
2471
|
+
--no-cache Skip build cache, force rebuild from source.
|
|
2472
|
+
--debug-guards Enable loop iteration and recursion guards (default: on in debug, off in release).
|
|
2473
|
+
--no-debug-guards Disable loop iteration and recursion guards.
|
|
2474
|
+
-I, --include-path PATH Add an extra module root.
|
|
2475
|
+
HELP
|
|
2476
|
+
"run-module" => <<~HELP,
|
|
2477
|
+
Usage: mtc run-module MODULE [OPTIONS] [-- ARGS...]
|
|
2478
|
+
|
|
2479
|
+
Resolve a standard-library module by name, build it, and run it.
|
|
2480
|
+
Like `python -m module.name`, this finds the module in the module
|
|
2481
|
+
roots (e.g., std/http/server.mt for `http/server`), compiles it, and
|
|
2482
|
+
forwards remaining arguments to the program.
|
|
2483
|
+
|
|
2484
|
+
Module name can use / or . separators (e.g., http/server or http.server).
|
|
2485
|
+
|
|
2486
|
+
Options:
|
|
2487
|
+
--cc COMPILER C compiler to use (default: $CC or cc).
|
|
2488
|
+
--profile PROFILE debug (default) | release.
|
|
2489
|
+
--platform PLATFORM linux (default) | windows | wasm.
|
|
2490
|
+
--locked Resolve dependencies from package.lock.
|
|
2491
|
+
--frozen Require a current package.lock and use locked resolution.
|
|
2492
|
+
-I, --include-path PATH Add an extra module root.
|
|
2493
|
+
|
|
2494
|
+
Arguments after -- are forwarded to the module as-is.
|
|
2495
|
+
HELP
|
|
2496
|
+
"toolchain" => <<~HELP,
|
|
2497
|
+
Usage: mtc toolchain SUBCOMMAND
|
|
2498
|
+
|
|
2499
|
+
Manage the local native toolchain and upstream native library checkouts.
|
|
2500
|
+
|
|
2501
|
+
Subcommands:
|
|
2502
|
+
bootstrap Download and prepare all upstream native libraries.
|
|
2503
|
+
doctor Check that all required tools and libraries are available.
|
|
2504
|
+
tools Build vendored tool binaries (e.g. tracy-profiler).
|
|
2505
|
+
HELP
|
|
2506
|
+
"deps" => <<~HELP,
|
|
2507
|
+
Usage: mtc deps SUBCOMMAND
|
|
2508
|
+
|
|
2509
|
+
Manage package dependencies and package.lock state.
|
|
2510
|
+
|
|
2511
|
+
Subcommands:
|
|
2512
|
+
add Add or update a package dependency and refresh package.lock.
|
|
2513
|
+
remove Remove a package dependency and refresh package.lock.
|
|
2514
|
+
update Re-resolve dependencies and refresh package.lock.
|
|
2515
|
+
tree Print the package dependency tree for a local package.
|
|
2516
|
+
lock Write or verify a deterministic package.lock for a local package.
|
|
2517
|
+
publish Publish an exact-version package into the local or configured upstream registry store.
|
|
2518
|
+
fetch Materialize cache-backed sources from package.lock explicitly.
|
|
2519
|
+
HELP
|
|
2520
|
+
"bindgen" => <<~HELP,
|
|
2521
|
+
Usage: mtc bindgen MODULE HEADER [OPTIONS]
|
|
2522
|
+
|
|
2523
|
+
Generate a Milk Tea binding module from a C header.
|
|
2524
|
+
|
|
2525
|
+
Options:
|
|
2526
|
+
-o, --output OUTPUT Write the generated module to this file.
|
|
2527
|
+
--nullable-report PATH Write the remaining manual nullable policy report to this file.
|
|
2528
|
+
--link LIB Link against this library (repeatable).
|
|
2529
|
+
--include HEADER Extra #include directive (repeatable).
|
|
2530
|
+
--clang PATH Clang binary to use (default: $CLANG or clang).
|
|
2531
|
+
--clang-arg ARG Extra argument to pass to clang (repeatable).
|
|
2532
|
+
HELP
|
|
2533
|
+
"cache" => <<~HELP,
|
|
2534
|
+
Usage: mtc cache SUBCOMMAND
|
|
2535
|
+
|
|
2536
|
+
Inspect and manage the build cache.
|
|
2537
|
+
|
|
2538
|
+
Subcommands:
|
|
2539
|
+
purge Remove the entire build cache.
|
|
2540
|
+
status Show cache directory, entry counts, and total size.
|
|
2541
|
+
HELP
|
|
2542
|
+
"docs" => <<~HELP,
|
|
2543
|
+
Usage: mtc docs [OPTIONS]
|
|
2544
|
+
|
|
2545
|
+
Start a local documentation server for Milk Tea with language reference
|
|
2546
|
+
and standard library exploration.
|
|
2547
|
+
|
|
2548
|
+
Options:
|
|
2549
|
+
--open, -o Open the docs in your default browser.
|
|
2550
|
+
--port, -p PORT Listen on a specific port (default: random).
|
|
2551
|
+
HELP
|
|
2552
|
+
"snapshot" => <<~HELP,
|
|
2553
|
+
Usage: mtc snapshot INPUT.mt [OPTIONS]
|
|
2554
|
+
|
|
2555
|
+
Generate an HTML snapshot of a Milk Tea source file with VS Code syntax
|
|
2556
|
+
highlighting using the active TextMate grammar and the configured theme.
|
|
2557
|
+
|
|
2558
|
+
Options:
|
|
2559
|
+
--theme, -t PATH VS Code theme JSON file (default: 2026 Dark).
|
|
2560
|
+
--output, -o PATH Write HTML to PATH instead of stdout.
|
|
2561
|
+
HELP
|
|
2562
|
+
"lsp" => <<~HELP,
|
|
2563
|
+
Usage: mtc lsp [OPTIONS]
|
|
2564
|
+
|
|
2565
|
+
Start the Milk Tea Language Server. Reads JSON-RPC messages from stdin
|
|
2566
|
+
and writes responses to stdout. Stderr is reserved for logging.
|
|
2567
|
+
|
|
2568
|
+
Options:
|
|
2569
|
+
--log-level LEVEL Log verbosity: trace, debug, info, warn, or
|
|
2570
|
+
error (default: warn, set by extension).
|
|
2571
|
+
--stdio Enable stdio transport (default).
|
|
2572
|
+
HELP
|
|
2573
|
+
"dap" => <<~HELP,
|
|
2574
|
+
Usage: mtc dap [OPTIONS]
|
|
2575
|
+
|
|
2576
|
+
Start the Milk Tea Debug Adapter. Reads DAP messages from stdin
|
|
2577
|
+
and writes responses to stdout.
|
|
2578
|
+
|
|
2579
|
+
Options:
|
|
2580
|
+
--backend KIND Debug backend kind (default: process).
|
|
2581
|
+
--adapter-path PATH Path to an lldb-dap or cppdbg adapter
|
|
2582
|
+
executable or Ruby bridge script.
|
|
2583
|
+
HELP
|
|
2584
|
+
"completions" => <<~HELP,
|
|
2585
|
+
Usage: mtc completions bash|zsh|fish
|
|
2586
|
+
|
|
2587
|
+
Print a shell completion script to stdout. Source it (or install it into
|
|
2588
|
+
your shell's completion directory) to get tab-completion for mtc commands.
|
|
2589
|
+
|
|
2590
|
+
Examples:
|
|
2591
|
+
mtc completions bash > /etc/bash_completion.d/mtc
|
|
2592
|
+
mtc completions zsh > ~/.zsh/completions/_mtc
|
|
2593
|
+
mtc completions fish > ~/.config/fish/completions/mtc.fish
|
|
2594
|
+
HELP
|
|
2595
|
+
}.freeze
|
|
2596
|
+
|
|
2597
|
+
def format_size(bytes)
|
|
2598
|
+
return "0 B" if bytes.zero?
|
|
2599
|
+
|
|
2600
|
+
units = %w[B KiB MiB GiB]
|
|
2601
|
+
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
2602
|
+
exp = units.length - 1 if exp >= units.length
|
|
2603
|
+
format("%.1f %s", bytes.to_f / (1024**exp), units[exp])
|
|
2604
|
+
end
|
|
2605
|
+
|
|
2606
|
+
def print_command_help(command, io)
|
|
2607
|
+
text = COMMAND_HELP[command]
|
|
2608
|
+
if text
|
|
2609
|
+
io.puts(text.chomp)
|
|
2610
|
+
else
|
|
2611
|
+
print_usage(io)
|
|
2612
|
+
end
|
|
2613
|
+
end
|
|
2614
|
+
|
|
2615
|
+
def print_toolchain_help(io)
|
|
2616
|
+
print_command_help("toolchain", io)
|
|
2617
|
+
end
|
|
2618
|
+
|
|
2619
|
+
def print_deps_help(io)
|
|
2620
|
+
print_command_help("deps", io)
|
|
2621
|
+
end
|
|
2622
|
+
|
|
2623
|
+
def print_bindgen_help(io)
|
|
2624
|
+
print_command_help("bindgen", io)
|
|
2625
|
+
end
|
|
2626
|
+
|
|
2627
|
+
def print_cache_help(io)
|
|
2628
|
+
print_command_help("cache", io)
|
|
2629
|
+
end
|
|
2630
|
+
|
|
2631
|
+
def print_usage(io)
|
|
2632
|
+
io.puts("Usage: mtc lex PATH")
|
|
2633
|
+
io.puts(" mtc parse PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-I PATH]")
|
|
2634
|
+
io.puts(" mtc format PATH|DIR [PATH|DIR ...] [--check|--write] [--safe|--canonical|--preserve|--tidy] [--max-line-length N] [--timings]")
|
|
2635
|
+
io.puts(" mtc lint PATH|DIR [--select RULES] [--ignore RULES] [--fix] [--ignore-generated] [--timings] [--locked] [--frozen] [-I PATH]")
|
|
2636
|
+
io.puts(" mtc lint --init")
|
|
2637
|
+
io.puts(" mtc check PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-Werror] [-I PATH]")
|
|
2638
|
+
io.puts(" mtc debug PATH [--locked] [--frozen] [-I PATH]")
|
|
2639
|
+
io.puts(" mtc lower PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-I PATH]")
|
|
2640
|
+
io.puts(" mtc emit-c PATH|DIR [PATH|DIR ...] [--locked] [--frozen] [-I PATH]")
|
|
2641
|
+
io.puts(" mtc build [PATH_OR_PACKAGE] [-o OUTPUT] [--cc COMPILER] [--keep-c C_PATH] [--profile debug|release] [--platform linux|windows|wasm] [--bundle] [--archive] [--locked] [--frozen] [--no-cache] [--debug-guards|--no-debug-guards] [--clean] [-I PATH]")
|
|
2642
|
+
io.puts(" mtc new NAME")
|
|
2643
|
+
io.puts(" mtc run [PATH_OR_PACKAGE] [-o OUTPUT] [--cc COMPILER] [--keep-c C_PATH] [--profile debug|release] [--platform linux|windows|wasm] [--locked] [--frozen] [-I PATH] [--debug-guards|--no-debug-guards]")
|
|
2644
|
+
io.puts(" mtc test PATH|DIR [--cc COMPILER] [--profile debug|release] [--platform linux|windows|wasm] [--timeout SECONDS] [--mem MB] [--jobs N] [--sanitize] [-n SUBSTRING] [--format human|tap|junit] [--locked] [--frozen] [-I PATH]")
|
|
2645
|
+
io.puts(" mtc run-module MODULE [--cc COMPILER] [--profile debug|release] [--platform linux|windows|wasm] [--locked] [--frozen] [-I PATH] [-- ARGS...]")
|
|
2646
|
+
io.puts(" mtc toolchain bootstrap")
|
|
2647
|
+
io.puts(" mtc toolchain doctor")
|
|
2648
|
+
io.puts(" mtc toolchain tools")
|
|
2649
|
+
io.puts(" mtc deps add [PATH_OR_PACKAGE] NAME[@VERSION_REQ] [--path PATH] [--git URL --rev REV [--subdir DIR]] [--version VERSION_REQ]")
|
|
2650
|
+
io.puts(" mtc deps remove [PATH_OR_PACKAGE] NAME")
|
|
2651
|
+
io.puts(" mtc deps update [PATH_OR_PACKAGE] [NAME ...]")
|
|
2652
|
+
io.puts(" mtc deps tree [PATH_OR_PACKAGE]")
|
|
2653
|
+
io.puts(" mtc deps lock [PATH_OR_PACKAGE] [--check]")
|
|
2654
|
+
io.puts(" mtc deps publish [PATH_OR_PACKAGE] [--upstream]")
|
|
2655
|
+
io.puts(" mtc deps fetch [PATH_OR_PACKAGE]")
|
|
2656
|
+
io.puts(" mtc bindgen MODULE HEADER [-o OUTPUT] [--nullable-report PATH] [--link LIB] [--include HEADER] [--clang PATH] [--clang-arg ARG]")
|
|
2657
|
+
io.puts(" mtc cache purge|status")
|
|
2658
|
+
io.puts(" mtc docs [--open] [--port PORT]")
|
|
2659
|
+
io.puts(" mtc snapshot INPUT.mt [--theme PATH] [-o OUTPUT]")
|
|
2660
|
+
io.puts(" mtc lsp [--log-level LEVEL] [--stdio]")
|
|
2661
|
+
io.puts(" mtc dap [--backend KIND] [--adapter-path PATH]")
|
|
2662
|
+
io.puts(" mtc completions bash|zsh|fish")
|
|
2663
|
+
end
|
|
2664
|
+
|
|
2665
|
+
def print_help(io)
|
|
2666
|
+
io.puts("mtc #{MilkTea::VERSION} \u2014 the Milk Tea compiler")
|
|
2667
|
+
io.puts
|
|
2668
|
+
io.puts("Usage: mtc <command> [options]")
|
|
2669
|
+
io.puts
|
|
2670
|
+
io.puts("Commands:")
|
|
2671
|
+
width = COMMANDS.map { |name, _| name.length }.max
|
|
2672
|
+
COMMANDS.each do |name, summary|
|
|
2673
|
+
io.puts(" #{name.ljust(width)} #{summary}")
|
|
2674
|
+
end
|
|
2675
|
+
io.puts
|
|
2676
|
+
io.puts("Global options:")
|
|
2677
|
+
io.puts(" -h, --help Show this help (use `mtc help <command>` for command help)")
|
|
2678
|
+
io.puts(" -V, --version Print the compiler version")
|
|
2679
|
+
io.puts(" -q, --quiet Suppress informational output")
|
|
2680
|
+
io.puts(" -v, --verbose Print per-file progress")
|
|
2681
|
+
io.puts(" --color WHEN Colorize diagnostics: auto (default), always, never")
|
|
2682
|
+
io.puts
|
|
2683
|
+
io.puts("Run `mtc <command> --help` for detailed usage of a command.")
|
|
2684
|
+
io.puts
|
|
2685
|
+
print_usage(io)
|
|
2686
|
+
end
|
|
2687
|
+
end
|
|
2688
|
+
end
|