mt-lang 0.3.16 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/docs/language-design.md +3 -4
- data/docs/language-manual.md +3 -3
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/{binding_types.rb → bindings.rb} +1 -1
- data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
- data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
- data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
- data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
- data/lib/milk_tea/core/c_backend.rb +63 -54
- data/lib/milk_tea/core/compile_time.rb +279 -74
- data/lib/milk_tea/core/control_flow/constant_propagation.rb +1 -1
- data/lib/milk_tea/core/{compatibility_helpers.rb → intrinsics.rb} +9 -2
- data/lib/milk_tea/core/lexer.rb +55 -48
- data/lib/milk_tea/core/lowering/block.rb +9 -0
- data/lib/milk_tea/core/lowering/calls.rb +2 -0
- data/lib/milk_tea/core/lowering/declarations.rb +1 -1
- data/lib/milk_tea/core/lowering/functions.rb +11 -8
- data/lib/milk_tea/core/lowering/resolve.rb +10 -3
- data/lib/milk_tea/core/lowering/scans.rb +13 -18
- data/lib/milk_tea/core/lowering.rb +18 -18
- data/lib/milk_tea/core/module_binder.rb +9 -10
- data/lib/milk_tea/core/module_loader.rb +64 -57
- data/lib/milk_tea/core/module_path_resolver.rb +1 -4
- data/lib/milk_tea/core/parser/declarations.rb +43 -19
- data/lib/milk_tea/core/parser/expressions.rb +4 -7
- data/lib/milk_tea/core/parser/statements.rb +5 -5
- data/lib/milk_tea/core/parser.rb +43 -17
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
- data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +117 -85
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
- data/lib/milk_tea/core/semantic_analyzer.rb +57 -38
- data/lib/milk_tea/core.rb +6 -2
- data/lib/milk_tea/dap/lldb_dap_backend.rb +153 -0
- data/lib/milk_tea/dap/server/lldb_backend.rb +1 -1
- data/lib/milk_tea/dap.rb +1 -1
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
- data/lib/milk_tea/lsp.rb +1 -7
- data/lib/milk_tea/{bindings/cli.rb → tooling/bindgen_cli.rb} +1 -1
- data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
- data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
- data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
- data/lib/milk_tea/tooling/cli/commands/check.rb +116 -0
- data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
- data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
- data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
- data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
- data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
- data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
- data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
- data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
- data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
- data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
- data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
- data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
- data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
- data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
- data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
- data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
- data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
- data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
- data/lib/milk_tea/tooling/cli.rb +101 -1893
- data/lib/milk_tea/tooling.rb +1 -1
- metadata +29 -9
- data/lib/milk_tea/core/compile_time/const_eval.rb +0 -182
- data/lib/milk_tea/core/module_loader/errors.rb +0 -23
- data/lib/milk_tea/dap/backends/lldb_dap.rb +0 -156
- /data/lib/milk_tea/core/lowering/{async.rb → async/frame_builder.rb} +0 -0
|
@@ -94,6 +94,7 @@ module MilkTea
|
|
|
94
94
|
end
|
|
95
95
|
ResolvedAttributeApplication = Data.define(:binding, :argument_values)
|
|
96
96
|
AttributePresenceKey = Data.define(:target, :attribute_module_name, :attribute_name)
|
|
97
|
+
CallableResolution = Data.define(:kind, :value, :receiver)
|
|
97
98
|
TypeParamConstraintBinding = Data.define(:interfaces) do
|
|
98
99
|
def initialize(interfaces: []) = super
|
|
99
100
|
end
|
|
@@ -129,7 +130,7 @@ module MilkTea
|
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
class Checker
|
|
132
|
-
include
|
|
133
|
+
include Intrinsics
|
|
133
134
|
|
|
134
135
|
attr_reader :ctx
|
|
135
136
|
|
|
@@ -185,30 +186,47 @@ module MilkTea
|
|
|
185
186
|
end
|
|
186
187
|
|
|
187
188
|
def check
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
189
|
+
@completed_phases = Set.new
|
|
190
|
+
|
|
191
|
+
run_phase(:install_builtin_types)
|
|
192
|
+
run_phase(:install_builtin_attributes)
|
|
193
|
+
run_phase(:install_imports)
|
|
194
|
+
run_phase(:install_prelude_types, requires: [:install_imports])
|
|
195
|
+
run_phase(:declare_named_types, requires: [:install_builtin_types, :install_imports, :install_prelude_types])
|
|
196
|
+
run_phase(:resolve_generic_type_param_constraints, requires: [:declare_named_types])
|
|
197
|
+
run_phase(:resolve_type_aliases, requires: [:declare_named_types])
|
|
198
|
+
run_phase(:declare_attributes)
|
|
199
|
+
run_phase(:resolve_aggregate_fields, requires: [:resolve_type_aliases, :declare_named_types])
|
|
200
|
+
run_phase(:resolve_enum_members, requires: [:declare_named_types])
|
|
201
|
+
run_phase(:resolve_variant_arms, requires: [:declare_named_types])
|
|
202
|
+
run_phase(:collect_emit_declarations)
|
|
203
|
+
run_phase(:declare_top_level_values, requires: [:resolve_aggregate_fields, :resolve_type_aliases])
|
|
204
|
+
run_phase(:check_attribute_applications, requires: [:declare_attributes])
|
|
205
|
+
run_phase(:declare_functions, requires: [:resolve_aggregate_fields, :resolve_enum_members, :resolve_variant_arms])
|
|
206
|
+
run_phase(:check_interface_conformances, requires: [:declare_functions, :resolve_aggregate_fields])
|
|
207
|
+
run_phase(:check_top_level_values, requires: [:declare_top_level_values])
|
|
208
|
+
run_phase(:finalize_top_level_const_values, requires: [:check_top_level_values])
|
|
209
|
+
run_phase(:check_top_level_static_asserts, requires: [:finalize_top_level_const_values])
|
|
210
|
+
run_phase(:check_functions, requires: [:declare_functions, :resolve_aggregate_fields, :check_interface_conformances])
|
|
208
211
|
|
|
209
212
|
build_analysis
|
|
210
213
|
end
|
|
211
214
|
|
|
215
|
+
def run_phase(name, requires: [])
|
|
216
|
+
requires.each do |required|
|
|
217
|
+
unless @completed_phases&.include?(required)
|
|
218
|
+
raise "BUG: phase #{required} must run before #{name} — check phase ordering"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
send(name)
|
|
222
|
+
ensure
|
|
223
|
+
@completed_phases << name if @completed_phases
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def run_collecting_phase(name, requires: [])
|
|
227
|
+
catch_structural { run_phase(name, requires:) }
|
|
228
|
+
end
|
|
229
|
+
|
|
212
230
|
def collect_emit_declarations
|
|
213
231
|
collect_emit_from_declarations(expanded_declarations)
|
|
214
232
|
@ctx.ast.declarations.grep(AST::ConstDecl).each { |decl| @ctx.const_declarations[decl.name] ||= decl }
|
|
@@ -274,23 +292,24 @@ module MilkTea
|
|
|
274
292
|
def check_collecting_errors
|
|
275
293
|
@collecting_errors = true
|
|
276
294
|
@structural_errors = []
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
295
|
+
@completed_phases = Set.new
|
|
296
|
+
|
|
297
|
+
run_collecting_phase(:install_builtin_types)
|
|
298
|
+
run_collecting_phase(:install_builtin_attributes)
|
|
299
|
+
run_collecting_phase(:install_imports)
|
|
300
|
+
run_collecting_phase(:install_prelude_types, requires: [:install_imports])
|
|
301
|
+
run_collecting_phase(:declare_named_types, requires: [:install_builtin_types, :install_imports, :install_prelude_types])
|
|
302
|
+
run_collecting_phase(:resolve_generic_type_param_constraints, requires: [:declare_named_types])
|
|
303
|
+
run_collecting_phase(:resolve_type_aliases, requires: [:declare_named_types])
|
|
304
|
+
run_collecting_phase(:declare_attributes)
|
|
305
|
+
run_collecting_phase(:resolve_aggregate_fields, requires: [:resolve_type_aliases, :declare_named_types])
|
|
306
|
+
run_collecting_phase(:resolve_enum_members, requires: [:declare_named_types])
|
|
307
|
+
run_collecting_phase(:resolve_variant_arms, requires: [:declare_named_types])
|
|
308
|
+
run_collecting_phase(:collect_emit_declarations)
|
|
309
|
+
run_collecting_phase(:declare_top_level_values, requires: [:resolve_aggregate_fields, :resolve_type_aliases])
|
|
310
|
+
run_collecting_phase(:check_attribute_applications, requires: [:declare_attributes])
|
|
311
|
+
run_collecting_phase(:declare_functions, requires: [:resolve_aggregate_fields, :resolve_enum_members, :resolve_variant_arms])
|
|
312
|
+
run_collecting_phase(:check_interface_conformances, requires: [:declare_functions, :resolve_aggregate_fields])
|
|
294
313
|
|
|
295
314
|
errors = @structural_errors.dup
|
|
296
315
|
|
data/lib/milk_tea/core.rb
CHANGED
|
@@ -10,11 +10,15 @@ require_relative "core/cst"
|
|
|
10
10
|
require_relative "core/cst_builder"
|
|
11
11
|
require_relative "core/ast"
|
|
12
12
|
require_relative "core/types"
|
|
13
|
-
require_relative "core/
|
|
13
|
+
require_relative "core/bindings"
|
|
14
14
|
require_relative "core/compile_time"
|
|
15
|
-
require_relative "core/
|
|
15
|
+
require_relative "core/intrinsics"
|
|
16
16
|
require_relative "core/parser"
|
|
17
17
|
require_relative "core/module_roots"
|
|
18
|
+
require_relative "core/module_path_resolver"
|
|
19
|
+
require_relative "core/module_binder"
|
|
20
|
+
require_relative "core/async_runtime_installer"
|
|
21
|
+
require_relative "core/prelude_installer"
|
|
18
22
|
require_relative "core/module_loader"
|
|
19
23
|
require_relative "core/semantic_analyzer"
|
|
20
24
|
require_relative "core/ir"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "thread"
|
|
5
|
+
require "timeout"
|
|
6
|
+
|
|
7
|
+
module MilkTea
|
|
8
|
+
module DAP
|
|
9
|
+
class LLDBDAPBackend
|
|
10
|
+
def initialize(adapter_command: ["lldb-dap"], on_event: nil, on_request: nil)
|
|
11
|
+
@adapter_command = adapter_command
|
|
12
|
+
@on_event = on_event
|
|
13
|
+
@on_request = on_request
|
|
14
|
+
@protocol = nil
|
|
15
|
+
@stdin = nil
|
|
16
|
+
@stdout = nil
|
|
17
|
+
@stderr = nil
|
|
18
|
+
@wait_thread = nil
|
|
19
|
+
@reader_thread = nil
|
|
20
|
+
@stderr_thread = nil
|
|
21
|
+
@write_mutex = Mutex.new
|
|
22
|
+
@pending_mutex = Mutex.new
|
|
23
|
+
@pending = {}
|
|
24
|
+
@next_seq = 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start!
|
|
28
|
+
return if running?
|
|
29
|
+
|
|
30
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(*@adapter_command)
|
|
31
|
+
@protocol = Protocol.new(input: @stdout, output: @stdin)
|
|
32
|
+
|
|
33
|
+
@reader_thread = Thread.new { read_loop }
|
|
34
|
+
@stderr_thread = Thread.new { drain_stderr }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def running?
|
|
38
|
+
!@wait_thread.nil? && @wait_thread.alive?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def request(command, arguments = {}, timeout: 5)
|
|
42
|
+
start! unless running?
|
|
43
|
+
|
|
44
|
+
seq = next_seq
|
|
45
|
+
queue = Queue.new
|
|
46
|
+
@pending_mutex.synchronize do
|
|
47
|
+
@pending[seq] = queue
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@write_mutex.synchronize do
|
|
51
|
+
@protocol.write_message({
|
|
52
|
+
seq: seq,
|
|
53
|
+
type: "request",
|
|
54
|
+
command: command,
|
|
55
|
+
arguments: arguments
|
|
56
|
+
})
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Timeout.timeout(timeout) { queue.pop }
|
|
60
|
+
rescue Timeout::Error
|
|
61
|
+
{
|
|
62
|
+
"type" => "response",
|
|
63
|
+
"request_seq" => seq,
|
|
64
|
+
"success" => false,
|
|
65
|
+
"message" => "backend request timed out: #{command}"
|
|
66
|
+
}
|
|
67
|
+
ensure
|
|
68
|
+
@pending_mutex.synchronize { @pending.delete(seq) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stop!
|
|
72
|
+
@stdin&.close
|
|
73
|
+
@stdout&.close
|
|
74
|
+
@stderr&.close
|
|
75
|
+
if @wait_thread&.alive?
|
|
76
|
+
Process.kill("TERM", @wait_thread.pid)
|
|
77
|
+
end
|
|
78
|
+
rescue StandardError
|
|
79
|
+
nil
|
|
80
|
+
ensure
|
|
81
|
+
@wait_thread&.join(0.2)
|
|
82
|
+
@reader_thread&.join(0.2)
|
|
83
|
+
@stderr_thread&.join(0.2)
|
|
84
|
+
@stdin = nil
|
|
85
|
+
@stdout = nil
|
|
86
|
+
@stderr = nil
|
|
87
|
+
@wait_thread = nil
|
|
88
|
+
@reader_thread = nil
|
|
89
|
+
@stderr_thread = nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def next_seq
|
|
93
|
+
seq = @next_seq
|
|
94
|
+
@next_seq += 1
|
|
95
|
+
seq
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def read_loop
|
|
99
|
+
loop do
|
|
100
|
+
message = @protocol.read_message
|
|
101
|
+
break if message.nil?
|
|
102
|
+
|
|
103
|
+
if message["type"] == "response"
|
|
104
|
+
queue = @pending_mutex.synchronize { @pending[message["request_seq"]] }
|
|
105
|
+
queue&.push(message)
|
|
106
|
+
elsif message["type"] == "event"
|
|
107
|
+
@on_event&.call(message)
|
|
108
|
+
elsif message["type"] == "request"
|
|
109
|
+
handle_adapter_request(message)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
114
|
+
ensure
|
|
115
|
+
@pending_mutex.synchronize do
|
|
116
|
+
@pending.each_value do |queue|
|
|
117
|
+
queue.push({
|
|
118
|
+
"type" => "response",
|
|
119
|
+
"success" => false,
|
|
120
|
+
"message" => "backend closed"
|
|
121
|
+
})
|
|
122
|
+
end
|
|
123
|
+
@pending.clear
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def drain_stderr
|
|
128
|
+
return unless @stderr
|
|
129
|
+
|
|
130
|
+
@stderr.each_line do |_line|
|
|
131
|
+
# Intentionally ignored in this bridge layer.
|
|
132
|
+
end
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def handle_adapter_request(message)
|
|
138
|
+
response = @on_request&.call(message)
|
|
139
|
+
response ||= {
|
|
140
|
+
"type" => "response",
|
|
141
|
+
"request_seq" => message["seq"],
|
|
142
|
+
"success" => false,
|
|
143
|
+
"command" => message["command"],
|
|
144
|
+
"message" => "unsupported reverse request: #{message['command']}"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@write_mutex.synchronize do
|
|
148
|
+
@protocol.write_message(response)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -22,7 +22,7 @@ module MilkTea
|
|
|
22
22
|
@lldb_backend = if @backend_factory
|
|
23
23
|
build_backend_via_factory(adapter_command)
|
|
24
24
|
else
|
|
25
|
-
|
|
25
|
+
LLDBDAPBackend.new(
|
|
26
26
|
adapter_command: adapter_command,
|
|
27
27
|
on_event: method(:handle_backend_event),
|
|
28
28
|
on_request: method(:handle_backend_request)
|
data/lib/milk_tea/dap.rb
CHANGED
|
@@ -556,9 +556,15 @@ module MilkTea
|
|
|
556
556
|
next_tok&.type == :colon
|
|
557
557
|
end
|
|
558
558
|
|
|
559
|
+
# Keywords whose standalone syntax is "keyword:" — they form blocks or
|
|
560
|
+
# expressions directly with a colon and no intervening token, so they are
|
|
561
|
+
# not field declarations when followed by ":".
|
|
562
|
+
STANDS_ALONE_WITH_COLON = %i[unsafe else defer parallel].to_set.freeze
|
|
563
|
+
|
|
559
564
|
def keyword_field_declaration_token?(tokens, index)
|
|
560
565
|
return false if match_arm_binding_token?(tokens, index)
|
|
561
566
|
return false if destructure_let_binding?(tokens, index)
|
|
567
|
+
return false if STANDS_ALONE_WITH_COLON.include?(tokens[index].type)
|
|
562
568
|
|
|
563
569
|
next_tok = next_non_trivia_token(tokens, index + 1)
|
|
564
570
|
next_tok&.type == :colon
|
data/lib/milk_tea/lsp.rb
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module MilkTea
|
|
4
|
-
module LSP
|
|
5
|
-
# LSP implementation - language server protocol support
|
|
6
|
-
end
|
|
7
|
-
end
|
|
8
|
-
|
|
9
3
|
require_relative 'lsp/protocol'
|
|
10
4
|
require_relative 'lsp/dependency_resolution'
|
|
11
5
|
require_relative 'lsp/workspace'
|
|
12
6
|
require_relative 'lsp/diagnostics'
|
|
13
|
-
require_relative 'lsp/server'
|
|
7
|
+
require_relative 'lsp/server'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class CLI
|
|
5
|
+
module CommandBuild
|
|
6
|
+
def build_command
|
|
7
|
+
path, options = extract_path_and_options(allow_clean: true)
|
|
8
|
+
return 1 unless path
|
|
9
|
+
|
|
10
|
+
if options.delete(:clean)
|
|
11
|
+
cleaned_path = Build.clean(path, output_path: options[:output_path], profile: options[:profile], platform: options[:platform], bundle: options[:bundle], archive: options[:archive])
|
|
12
|
+
info("cleaned #{cleaned_path}")
|
|
13
|
+
return 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
frozen = options.delete(:frozen)
|
|
17
|
+
ensure_current_lockfile!(path) if frozen
|
|
18
|
+
locked = options.delete(:locked)
|
|
19
|
+
bundle = options[:bundle]
|
|
20
|
+
package_graph = package_graph_for(path, locked:)
|
|
21
|
+
result = Build.build(path, module_roots: module_roots_for(path, locked:), package_graph:, frontend: @build_frontend, **options.except(:timings))
|
|
22
|
+
if bundle
|
|
23
|
+
info("built #{path} -> #{File.dirname(result.output_path)}")
|
|
24
|
+
info("entry executable #{result.output_path}")
|
|
25
|
+
info(" [cached]") if result.cached
|
|
26
|
+
info("archive #{result.archive_path}") if result.archive_path
|
|
27
|
+
elsif result.cached
|
|
28
|
+
info("built #{path} -> #{result.output_path} [cached]")
|
|
29
|
+
else
|
|
30
|
+
info("built #{path} -> #{result.output_path}")
|
|
31
|
+
end
|
|
32
|
+
info("saved C to #{result.c_path}") if result.c_path
|
|
33
|
+
0
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class CLI
|
|
5
|
+
module CommandCache
|
|
6
|
+
def cache_command
|
|
7
|
+
subcommand = @argv.shift
|
|
8
|
+
unless subcommand
|
|
9
|
+
@err.puts("missing cache subcommand")
|
|
10
|
+
print_command_help("cache", @err)
|
|
11
|
+
return 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
cache_root = MilkTea.data_root.join("tmp", "mtc-cache")
|
|
15
|
+
|
|
16
|
+
case subcommand
|
|
17
|
+
when "purge"
|
|
18
|
+
if File.directory?(cache_root)
|
|
19
|
+
FileUtils.rm_rf(cache_root)
|
|
20
|
+
@out.puts("purged #{cache_root}")
|
|
21
|
+
else
|
|
22
|
+
@out.puts("cache is already empty")
|
|
23
|
+
end
|
|
24
|
+
0
|
|
25
|
+
when "status"
|
|
26
|
+
unless File.directory?(cache_root)
|
|
27
|
+
@out.puts("cache directory does not exist: #{cache_root}")
|
|
28
|
+
return 0
|
|
29
|
+
end
|
|
30
|
+
program_dirs = Dir.glob(File.join(cache_root, "programs", "*", "*")).select { |d| File.directory?(d) }
|
|
31
|
+
binary_files = Dir.glob(File.join(cache_root, "binaries", "*", "*", "binary")).select { |f| File.file?(f) }
|
|
32
|
+
total_size = (program_dirs + binary_files).sum { |p|
|
|
33
|
+
File.file?(p) ? File.size(p) : Dir.glob(File.join(p, "**", "*")).sum { |f| File.file?(f) ? File.size(f) : 0 }
|
|
34
|
+
}
|
|
35
|
+
@out.puts("cache #{program_dirs.length} programs, #{binary_files.length} binaries (#{format_size(total_size)})")
|
|
36
|
+
@out.puts(" root #{cache_root}")
|
|
37
|
+
0
|
|
38
|
+
else
|
|
39
|
+
@err.puts("unknown cache subcommand #{subcommand}")
|
|
40
|
+
print_command_help("cache", @err)
|
|
41
|
+
1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class CLI
|
|
5
|
+
module CommandCheck
|
|
6
|
+
def check_command
|
|
7
|
+
args = @argv.dup
|
|
8
|
+
@argv = []
|
|
9
|
+
until args.empty?
|
|
10
|
+
arg = args.shift
|
|
11
|
+
@argv << arg
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
unless @argv.any?
|
|
15
|
+
@err.puts("missing source file path")
|
|
16
|
+
print_usage(@err)
|
|
17
|
+
return 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
resolution = extract_resolution_flags!
|
|
21
|
+
input_paths = @argv.dup
|
|
22
|
+
return 1 unless ensure_known_source_operands!("check", input_paths)
|
|
23
|
+
|
|
24
|
+
paths = expand_source_paths(input_paths)
|
|
25
|
+
return 0 if print_no_source_files_if_empty(paths, input_paths)
|
|
26
|
+
|
|
27
|
+
ensure_current_lockfiles!(paths) if resolution[:frozen]
|
|
28
|
+
|
|
29
|
+
all_diagnostics = []
|
|
30
|
+
paths.each do |path|
|
|
31
|
+
diagnostics, module_name, closure_errors = check_single_reporting_all(path, locked: resolution[:locked])
|
|
32
|
+
closure_errors = [] if paths.length > 1
|
|
33
|
+
diagnostics = sort_by_location(diagnostics)
|
|
34
|
+
|
|
35
|
+
if diagnostics.any? || closure_errors.any?
|
|
36
|
+
main_source = read_source_file(path)
|
|
37
|
+
main_abs = File.expand_path(path)
|
|
38
|
+
diagnostics.each do |d|
|
|
39
|
+
same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
|
|
40
|
+
source = same_file ? main_source : nil
|
|
41
|
+
@err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
|
|
42
|
+
end
|
|
43
|
+
closure_errors.each do |d|
|
|
44
|
+
same_file = !d.respond_to?(:path) || d.path.nil? || File.expand_path(d.path) == main_abs
|
|
45
|
+
source = same_file ? main_source : nil
|
|
46
|
+
@err.puts(ErrorFormatter.format(d, source:, color: error_color?(@err)))
|
|
47
|
+
end
|
|
48
|
+
all_diagnostics.concat(diagnostics)
|
|
49
|
+
all_diagnostics.concat(closure_errors)
|
|
50
|
+
elsif module_name
|
|
51
|
+
info("checked #{path} as #{module_name}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return 0 if all_diagnostics.empty?
|
|
56
|
+
|
|
57
|
+
error_count = all_diagnostics.count { |d| !d.respond_to?(:severity) || d.severity == :error }
|
|
58
|
+
warning_count = all_diagnostics.count { |d| d.respond_to?(:severity) && d.severity == :warning }
|
|
59
|
+
info_count = all_diagnostics.count { |d| d.respond_to?(:severity) && (d.severity == :info || d.severity == :hint) }
|
|
60
|
+
|
|
61
|
+
@err.puts
|
|
62
|
+
parts = []
|
|
63
|
+
parts << "#{error_count} #{error_count == 1 ? 'error' : 'errors'}" if error_count > 0
|
|
64
|
+
parts << "#{warning_count} #{warning_count == 1 ? 'warning' : 'warnings'}" if warning_count > 0
|
|
65
|
+
parts << "#{info_count} #{info_count == 1 ? 'note' : 'notes'}" if info_count > 0
|
|
66
|
+
body = parts.join("; ")
|
|
67
|
+
if error_count > 0
|
|
68
|
+
@err.puts("#{body} found")
|
|
69
|
+
elsif warning_count > 0
|
|
70
|
+
@err.puts("#{body}")
|
|
71
|
+
end
|
|
72
|
+
final_error_count = error_count + (resolution[:warnings_as_errors] ? warning_count : 0)
|
|
73
|
+
final_error_count > 0 ? 1 : 0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def check_single_reporting_all(path, locked: false)
|
|
77
|
+
loader = make_module_loader(path, locked:, platform: ModuleLoader.default_host_platform)
|
|
78
|
+
resolved_path = File.expand_path(path)
|
|
79
|
+
ast = loader.load_file(resolved_path)
|
|
80
|
+
module_name = ast.module_name.to_s
|
|
81
|
+
|
|
82
|
+
import_result = loader.imported_modules_for_ast_collecting_errors(ast, importer_path: resolved_path)
|
|
83
|
+
errors = import_result.errors.dup
|
|
84
|
+
|
|
85
|
+
analysis = nil
|
|
86
|
+
begin
|
|
87
|
+
result = SemanticAnalyzer.check_collecting_errors(ast, imported_modules: import_result.modules, path: resolved_path)
|
|
88
|
+
errors.concat(result[:errors])
|
|
89
|
+
analysis = result[:analysis]
|
|
90
|
+
rescue SemanticError => e
|
|
91
|
+
errors << e
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if analysis && errors.empty?
|
|
95
|
+
source = read_source_file(path)
|
|
96
|
+
warnings = Linter.lint_source(source, path: resolved_path, sema_facts: analysis, lint_tier: :full)
|
|
97
|
+
errors.concat(warnings)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
closure_errors = loader.collecting_path_errors.values.flatten.compact
|
|
101
|
+
[errors, module_name, closure_errors]
|
|
102
|
+
rescue ModuleLoadError, PackageLockError, SemanticError => e
|
|
103
|
+
[[e], nil, []]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def sort_by_location(errors)
|
|
107
|
+
errors.sort_by do |e|
|
|
108
|
+
actual = e.respond_to?(:error) ? e.error : e
|
|
109
|
+
line = actual.respond_to?(:line) ? actual.line.to_i : 0
|
|
110
|
+
column = actual.respond_to?(:column) ? actual.column.to_i : 0
|
|
111
|
+
[line, column]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
class CLI
|
|
5
|
+
module CommandCompletions
|
|
6
|
+
def completions_command
|
|
7
|
+
shell = @argv.shift
|
|
8
|
+
unless %w[bash zsh fish].include?(shell)
|
|
9
|
+
@err.puts("completions: shell must be bash, zsh, or fish")
|
|
10
|
+
print_command_help("completions", @err)
|
|
11
|
+
return 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
@out.puts(completion_script(shell))
|
|
15
|
+
0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def completion_script(shell)
|
|
19
|
+
names = COMMANDS.map(&:first)
|
|
20
|
+
case shell
|
|
21
|
+
when "bash"
|
|
22
|
+
[
|
|
23
|
+
"# mtc bash completion. Source this file or install it into your bash",
|
|
24
|
+
"# completion directory (e.g. /etc/bash_completion.d/mtc).",
|
|
25
|
+
"_mtc() {",
|
|
26
|
+
%( local cur="${COMP_WORDS[COMP_CWORD]}"),
|
|
27
|
+
%( if [ "${COMP_CWORD}" -eq 1 ]; then),
|
|
28
|
+
%( COMPREPLY=( $(compgen -W "#{(names + %w[help version]).join(' ')}" -- "${cur}") )),
|
|
29
|
+
" fi",
|
|
30
|
+
"}",
|
|
31
|
+
"complete -F _mtc mtc",
|
|
32
|
+
].join("\n")
|
|
33
|
+
when "zsh"
|
|
34
|
+
lines = ["#compdef mtc", "# mtc zsh completion. Install onto your $fpath as _mtc.", "_mtc() {", " local -a commands", " commands=("]
|
|
35
|
+
COMMANDS.each { |name, summary| lines << " '#{name}:#{summary}'" }
|
|
36
|
+
lines.concat([" )", " if (( CURRENT == 2 )); then", " _describe 'mtc command' commands", " fi", "}", %(_mtc "$@")])
|
|
37
|
+
lines.join("\n")
|
|
38
|
+
when "fish"
|
|
39
|
+
lines = ["# mtc fish completion. Install into ~/.config/fish/completions/mtc.fish."]
|
|
40
|
+
COMMANDS.each do |name, summary|
|
|
41
|
+
lines << "complete -c mtc -f -n '__fish_use_subcommand' -a #{name} -d '#{summary}'"
|
|
42
|
+
end
|
|
43
|
+
lines.join("\n")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|