mt-lang 0.4.1 → 0.4.21
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/README.md +11 -1
- data/docs/index.html +76 -2
- data/docs/language-design.md +2 -2
- data/docs/language-manual.md +15 -3
- data/docs/testing.md +91 -0
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/control_flow/builder.rb +10 -1
- data/lib/milk_tea/core/lowering/assertions.rb +100 -0
- data/lib/milk_tea/core/lowering/block.rb +5 -0
- data/lib/milk_tea/core/lowering/calls.rb +2 -0
- data/lib/milk_tea/core/lowering/resolve.rb +4 -0
- data/lib/milk_tea/core/lowering.rb +2 -0
- data/lib/milk_tea/core/semantic_analyzer/calls.rb +31 -0
- data/lib/milk_tea/core/semantic_analyzer/expressions.rb +8 -0
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +2 -1
- data/lib/milk_tea/lsp/server/completion.rb +9 -18
- data/lib/milk_tea/lsp/server/utilities.rb +2 -3
- data/lib/milk_tea/lsp/workspace/caches.rb +2 -0
- data/lib/milk_tea/lsp/workspace/definition_index.rb +12 -13
- data/lib/milk_tea/lsp/workspace/store.rb +6 -1
- data/lib/milk_tea/lsp/workspace/utilities.rb +50 -30
- data/lib/milk_tea/lsp/workspace.rb +5 -1
- data/lib/milk_tea/tooling/cli/commands/run.rb +3 -2
- data/lib/milk_tea/tooling/cli/commands/test.rb +39 -40
- data/lib/milk_tea/tooling/cli.rb +5 -4
- data/lib/milk_tea/tooling/linter/rules.rb +13 -2
- metadata +4 -3
- data/std/testing.mt +0 -266
|
@@ -190,7 +190,8 @@ module MilkTea
|
|
|
190
190
|
"return type mismatch: expected #{return_type}, got #{value_type}",
|
|
191
191
|
expression: statement.value,
|
|
192
192
|
contextual_int_to_float: contextual_int_to_float_target?(return_type),
|
|
193
|
-
line: statement.line,
|
|
193
|
+
line: (statement.line unless statement.value),
|
|
194
|
+
column: (statement.column unless statement.value),
|
|
194
195
|
)
|
|
195
196
|
when AST::DeferStmt
|
|
196
197
|
with_loop_barrier do
|
|
@@ -459,8 +459,8 @@ module MilkTea
|
|
|
459
459
|
return nil if prefix.empty?
|
|
460
460
|
return nil unless @current_completion_trigger_kind == TRIGGER_KIND_INCOMPLETE
|
|
461
461
|
|
|
462
|
-
|
|
463
|
-
line_prefix = (
|
|
462
|
+
lines = @workspace.document_lines(uri)
|
|
463
|
+
line_prefix = (lines[lsp_line] || '')[0...lsp_char]
|
|
464
464
|
cached = @completion_session_cache[[uri, lsp_line]]
|
|
465
465
|
return nil unless cached
|
|
466
466
|
return nil unless line_prefix.start_with?(cached[:line_prefix])
|
|
@@ -473,8 +473,8 @@ module MilkTea
|
|
|
473
473
|
def store_completion_session(uri, lsp_line, lsp_char, prefix, response)
|
|
474
474
|
return if prefix.empty?
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
line_prefix = (
|
|
476
|
+
lines = @workspace.document_lines(uri)
|
|
477
|
+
line_prefix = (lines[lsp_line] || '')[0...lsp_char]
|
|
478
478
|
key = [uri, lsp_line]
|
|
479
479
|
@completion_session_cache[key] = {
|
|
480
480
|
line_prefix: line_prefix,
|
|
@@ -875,8 +875,7 @@ module MilkTea
|
|
|
875
875
|
end
|
|
876
876
|
|
|
877
877
|
def import_completions(uri, lsp_line, lsp_char)
|
|
878
|
-
|
|
879
|
-
lines = content.split("\n", -1)
|
|
878
|
+
lines = @workspace.document_lines(uri)
|
|
880
879
|
line = lines[lsp_line] || ''
|
|
881
880
|
stripped = line.lstrip
|
|
882
881
|
return nil unless stripped.start_with?('import ')
|
|
@@ -936,9 +935,7 @@ module MilkTea
|
|
|
936
935
|
end
|
|
937
936
|
|
|
938
937
|
def attribute_completions(facts, uri, line, char)
|
|
939
|
-
|
|
940
|
-
return nil unless content
|
|
941
|
-
lines = content.split("\n", -1)
|
|
938
|
+
lines = @workspace.document_lines(uri)
|
|
942
939
|
line_text = lines[line] || ''
|
|
943
940
|
return nil if line_text.empty?
|
|
944
941
|
|
|
@@ -980,9 +977,7 @@ module MilkTea
|
|
|
980
977
|
def format_string_completions(facts, uri, line, char)
|
|
981
978
|
return nil unless facts
|
|
982
979
|
|
|
983
|
-
|
|
984
|
-
return nil unless content
|
|
985
|
-
lines = content.split("\n", -1)
|
|
980
|
+
lines = @workspace.document_lines(uri)
|
|
986
981
|
line_text = lines[line] || ''
|
|
987
982
|
return nil if line_text.empty?
|
|
988
983
|
|
|
@@ -1045,9 +1040,7 @@ module MilkTea
|
|
|
1045
1040
|
def named_argument_completions(facts, uri, line, char)
|
|
1046
1041
|
return nil unless facts
|
|
1047
1042
|
|
|
1048
|
-
|
|
1049
|
-
return nil unless content
|
|
1050
|
-
lines = content.split("\n", -1)
|
|
1043
|
+
lines = @workspace.document_lines(uri)
|
|
1051
1044
|
line_text = lines[line] || ''
|
|
1052
1045
|
return nil if line_text.empty?
|
|
1053
1046
|
|
|
@@ -1109,9 +1102,7 @@ module MilkTea
|
|
|
1109
1102
|
def specialization_completions(facts, _uri, line, char)
|
|
1110
1103
|
return nil unless facts
|
|
1111
1104
|
|
|
1112
|
-
|
|
1113
|
-
return nil unless content
|
|
1114
|
-
lines = content.split("\n", -1)
|
|
1105
|
+
lines = @workspace.document_lines(_uri)
|
|
1115
1106
|
line_text = lines[line] || ''
|
|
1116
1107
|
return nil if line_text.empty?
|
|
1117
1108
|
|
|
@@ -295,9 +295,8 @@ module MilkTea
|
|
|
295
295
|
end
|
|
296
296
|
|
|
297
297
|
def current_word_prefix(uri, lsp_line, lsp_char)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
line = lines[lsp_line] || ''
|
|
298
|
+
lines = @workspace.document_lines(uri)
|
|
299
|
+
line = lines[lsp_line] || ''
|
|
301
300
|
# Walk backwards from cursor to find start of current word
|
|
302
301
|
char_idx = [lsp_char - 1, line.length - 1].min
|
|
303
302
|
return '' if char_idx < 0
|
|
@@ -284,6 +284,8 @@ module MilkTea
|
|
|
284
284
|
def invalidate_cache(uri, clear_last_good: false)
|
|
285
285
|
@tokens_cache.delete(uri)
|
|
286
286
|
@ast_cache.delete(uri)
|
|
287
|
+
@definition_token_index.delete(uri)
|
|
288
|
+
@line_cache.delete(uri)
|
|
287
289
|
@symbols_cache.delete(uri)
|
|
288
290
|
@doc_comments_cache.delete(uri)
|
|
289
291
|
@facts_cache_mutex.synchronize do
|
|
@@ -48,7 +48,6 @@ module MilkTea
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
50
|
def candidate_definition_uris(name, exclude_uri: nil)
|
|
51
|
-
matcher = definition_line_matcher(name)
|
|
52
51
|
open_uris, indexed_uris = @document_state_mutex.synchronize do
|
|
53
52
|
[@open_documents.keys, @indexed_documents.keys]
|
|
54
53
|
end
|
|
@@ -57,33 +56,33 @@ module MilkTea
|
|
|
57
56
|
warmed_candidates = nil
|
|
58
57
|
warmed_uris = nil
|
|
59
58
|
@definition_cache_mutex.synchronize do
|
|
60
|
-
warmed_candidates = @definition_candidate_uris[name].
|
|
59
|
+
warmed_candidates = @definition_candidate_uris[name].to_a
|
|
61
60
|
warmed_uris = @definition_names_by_uri.keys.to_set
|
|
62
61
|
end
|
|
63
62
|
|
|
64
|
-
matches = warmed_candidates.
|
|
65
|
-
next if doc_uri == exclude_uri
|
|
66
|
-
|
|
67
|
-
doc_uri
|
|
68
|
-
end
|
|
63
|
+
matches = warmed_candidates.reject { |doc_uri| doc_uri == exclude_uri }
|
|
69
64
|
|
|
65
|
+
# One-time lazy index: extract each document's definition names and
|
|
66
|
+
# cache them so repeat lookups are Set-membership checks instead of
|
|
67
|
+
# re-scanning every indexed document's full content with a regex on
|
|
68
|
+
# each lookup (which scaled with total workspace bytes).
|
|
70
69
|
ordered_uris.each do |doc_uri|
|
|
71
70
|
next if doc_uri == exclude_uri
|
|
72
71
|
next if warmed_uris.include?(doc_uri)
|
|
73
72
|
|
|
74
73
|
content = get_content(doc_uri)
|
|
75
74
|
next if content.empty?
|
|
76
|
-
next unless content.match?(matcher)
|
|
77
75
|
|
|
78
76
|
warm_definition_candidates_for_uri(doc_uri, content)
|
|
79
|
-
matches << doc_uri
|
|
80
77
|
end
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
@definition_cache_mutex.synchronize do
|
|
80
|
+
@definition_candidate_uris[name].each do |doc_uri|
|
|
81
|
+
matches << doc_uri unless doc_uri == exclude_uri
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
/#{DEFINITION_LINE_PREFIX}#{Regexp.escape(name)}\b/
|
|
85
|
+
matches.uniq
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def cache_definition_entry(name, entry)
|
|
@@ -126,13 +126,18 @@ module MilkTea
|
|
|
126
126
|
total = paths.length
|
|
127
127
|
paths.each_with_index do |path, idx|
|
|
128
128
|
file_uri = path_to_uri(path)
|
|
129
|
+
content = nil
|
|
129
130
|
@document_state_mutex.synchronize do
|
|
130
|
-
@indexed_documents[file_uri] ||= begin
|
|
131
|
+
content = @indexed_documents[file_uri] ||= begin
|
|
131
132
|
File.read(path)
|
|
132
133
|
rescue StandardError
|
|
133
134
|
nil
|
|
134
135
|
end
|
|
135
136
|
end
|
|
137
|
+
# Warm the definition-name index for this file so later global
|
|
138
|
+
# definition lookups hit Set-membership checks instead of scanning
|
|
139
|
+
# every indexed document's content.
|
|
140
|
+
warm_definition_candidates_for_uri(file_uri, content) if content
|
|
136
141
|
if progress && total > 0
|
|
137
142
|
pct = ((idx + 1) * 100 / total).clamp(0, 100)
|
|
138
143
|
progress.call(pct, "#{idx + 1}/#{total} files")
|
|
@@ -79,10 +79,9 @@ module MilkTea
|
|
|
79
79
|
# Scan text up to the cursor to find the innermost open function call context.
|
|
80
80
|
# Returns { name:, active_parameter: } or nil if not inside a call.
|
|
81
81
|
def find_call_context(uri, lsp_line, lsp_char)
|
|
82
|
-
|
|
83
|
-
return nil if
|
|
82
|
+
lines = document_lines(uri)
|
|
83
|
+
return nil if lines.empty?
|
|
84
84
|
|
|
85
|
-
lines = content.split("\n", -1)
|
|
86
85
|
cursor_line = lines[lsp_line] || ''
|
|
87
86
|
prefix = lsp_line.positive? ? lines[0...lsp_line].join("\n") + "\n" : ''
|
|
88
87
|
text = prefix + cursor_line[0...lsp_char]
|
|
@@ -150,14 +149,19 @@ module MilkTea
|
|
|
150
149
|
end
|
|
151
150
|
|
|
152
151
|
def token_contains_position?(token, target_line, target_char)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
return false if target_line < token.line
|
|
153
|
+
|
|
154
|
+
lexeme = token.lexeme
|
|
155
|
+
unless lexeme.include?("\n")
|
|
156
|
+
return false if target_line > token.line
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
return token.column <= target_char && target_char < (token.column + segments.first.length)
|
|
158
|
+
return token.column <= target_char && target_char < (token.column + lexeme.length)
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
+
segments = lexeme.split("\n", -1)
|
|
162
|
+
end_line = token.line + segments.length - 1
|
|
163
|
+
return false if target_line > end_line
|
|
164
|
+
|
|
161
165
|
if target_line == token.line
|
|
162
166
|
return token.column <= target_char && target_char <= (token.column + segments.first.length - 1)
|
|
163
167
|
end
|
|
@@ -178,8 +182,7 @@ module MilkTea
|
|
|
178
182
|
end
|
|
179
183
|
|
|
180
184
|
def find_dot_receiver_path(uri, lsp_line, lsp_char)
|
|
181
|
-
|
|
182
|
-
lines = content.split("\n", -1)
|
|
185
|
+
lines = document_lines(uri)
|
|
183
186
|
line_str = lines[lsp_line] || ''
|
|
184
187
|
|
|
185
188
|
idx = [lsp_char - 1, line_str.length - 1].min
|
|
@@ -222,39 +225,56 @@ module MilkTea
|
|
|
222
225
|
# (def, struct, union, enum, flags, variant, type, const, var) for the given name.
|
|
223
226
|
# Returns the identifier Token, or nil if not found.
|
|
224
227
|
def find_definition_token(uri, name, before_line: nil, before_char: nil)
|
|
225
|
-
|
|
226
|
-
return nil if
|
|
227
|
-
|
|
228
|
-
nearest = nil
|
|
229
|
-
tokens.each_cons(2) do |kw_tok, id_tok|
|
|
230
|
-
next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
|
|
231
|
-
next unless id_tok.type == :identifier && id_tok.lexeme == name
|
|
228
|
+
candidates = definition_token_index(uri)[name]
|
|
229
|
+
return nil if candidates.nil? || candidates.empty?
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
if nearest.nil? || id_tok.line > nearest.line || (id_tok.line == nearest.line && id_tok.column > nearest.column)
|
|
239
|
-
nearest = id_tok
|
|
231
|
+
if before_line
|
|
232
|
+
matches = candidates.select do |tok|
|
|
233
|
+
tok.line < before_line || (tok.line == before_line && (!before_char || tok.column < before_char))
|
|
240
234
|
end
|
|
235
|
+
return matches.max_by { |tok| [tok.line, tok.column] } unless matches.empty?
|
|
241
236
|
end
|
|
242
237
|
|
|
243
|
-
|
|
238
|
+
candidates.first
|
|
239
|
+
end
|
|
244
240
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
# Return the cached line array for +uri+, so hot request paths (e.g.
|
|
242
|
+
# completion) do not re-split the whole document once per helper.
|
|
243
|
+
def document_lines(uri)
|
|
244
|
+
@document_state_mutex.synchronize do
|
|
245
|
+
return @line_cache[uri] if @line_cache.key?(uri)
|
|
246
|
+
end
|
|
248
247
|
|
|
249
|
-
|
|
248
|
+
lines = get_content(uri).split("\n", -1)
|
|
249
|
+
@document_state_mutex.synchronize do
|
|
250
|
+
@line_cache[uri] = lines
|
|
250
251
|
end
|
|
251
|
-
|
|
252
|
+
lines
|
|
252
253
|
end
|
|
253
254
|
|
|
254
255
|
private
|
|
255
256
|
|
|
256
257
|
# ── Symbol extraction (token-based, no AST position requirement) ────────
|
|
257
258
|
|
|
259
|
+
# Lazily build a per-uri map of definition identifier tokens keyed by
|
|
260
|
+
# name, so repeated definition lookups are O(definitions-with-name)
|
|
261
|
+
# instead of re-scanning the document's full token stream each time.
|
|
262
|
+
def definition_token_index(uri)
|
|
263
|
+
@definition_token_index[uri] ||= begin
|
|
264
|
+
tokens = get_tokens(uri)
|
|
265
|
+
index = Hash.new { |hash, key| hash[key] = [] }
|
|
266
|
+
if tokens
|
|
267
|
+
tokens.each_cons(2) do |kw_tok, id_tok|
|
|
268
|
+
next unless DEFINITION_KEYWORDS.include?(kw_tok.type)
|
|
269
|
+
next unless id_tok.type == :identifier
|
|
270
|
+
|
|
271
|
+
index[id_tok.lexeme] << id_tok
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
index
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
258
278
|
def extract_symbols_from_tokens(uri)
|
|
259
279
|
tokens = get_tokens(uri)
|
|
260
280
|
return [] if tokens.nil?
|
|
@@ -37,7 +37,7 @@ module MilkTea
|
|
|
37
37
|
# adjacent tokens, and the inner keyword (e.g. :function) is always followed
|
|
38
38
|
# by the identifier. If the lexer ever merges a compound keyword into a
|
|
39
39
|
# single token type (e.g. :const_function), it must be added here.
|
|
40
|
-
DEFINITION_KEYWORDS = %i[function struct union enum flags variant type const var let extending opaque interface event].freeze
|
|
40
|
+
DEFINITION_KEYWORDS = %i[function struct union enum flags variant type const var let extending opaque interface event].to_set.freeze
|
|
41
41
|
DOC_COMMENT_PREFIX = '##'
|
|
42
42
|
DOC_TAG_PATTERN = /\A\s*@([A-Za-z_][A-Za-z0-9_-]*)(?:\s+(.*))?\z/
|
|
43
43
|
DEFINITION_LINE_PREFIX = /^(?:\s)*(?:(?:public|foreign|external)\s+)*(?:function|struct|union|enum|flags|variant|type|const|var|let|extending|opaque|interface|event)\s+/m
|
|
@@ -55,6 +55,8 @@ module MilkTea
|
|
|
55
55
|
@tokens_cache = {} # uri -> [Token]
|
|
56
56
|
@last_good_tokens_cache = {} # uri -> last known-good [Token]
|
|
57
57
|
@ast_cache = {} # uri -> AST::SourceFile (nil on parse failure)
|
|
58
|
+
@definition_token_index = {} # uri -> { name => [Token] } definition tokens by name
|
|
59
|
+
@line_cache = {} # uri -> [String] split lines, refreshed on invalidation
|
|
58
60
|
@facts_cache = {} # uri -> SemanticAnalyzer::Facts (projection of cached tooling snapshot facts)
|
|
59
61
|
@tooling_snapshot_cache = {} # uri -> SemanticAnalyzer::ToolingSnapshot (facts may be nil on structural failure)
|
|
60
62
|
@symbols_cache = {} # uri -> [{name, kind, line, column}]
|
|
@@ -103,6 +105,8 @@ module MilkTea
|
|
|
103
105
|
@tokens_cache.clear
|
|
104
106
|
@last_good_tokens_cache.clear
|
|
105
107
|
@ast_cache.clear
|
|
108
|
+
@definition_token_index.clear
|
|
109
|
+
@line_cache.clear
|
|
106
110
|
@facts_cache.clear
|
|
107
111
|
@tooling_snapshot_cache.clear
|
|
108
112
|
@symbols_cache.clear
|
|
@@ -72,11 +72,12 @@ module MilkTea
|
|
|
72
72
|
argv: @argv.dup,
|
|
73
73
|
**options.except(:timings)
|
|
74
74
|
)
|
|
75
|
-
|
|
75
|
+
live = $stdout.tty?
|
|
76
|
+
unless (@out.equal?($stdout) && live) || preview_notice_emitted
|
|
76
77
|
@out.write(result.stdout)
|
|
77
78
|
end
|
|
78
79
|
@out.flush if @out.respond_to?(:flush)
|
|
79
|
-
@err.write(result.stderr) unless @err.equal?($stderr)
|
|
80
|
+
@err.write(result.stderr) unless @err.equal?($stderr) && live
|
|
80
81
|
info("[cached]") if result.cached
|
|
81
82
|
result.exit_status
|
|
82
83
|
end
|
|
@@ -399,21 +399,14 @@ module MilkTea
|
|
|
399
399
|
return 1
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
-
testing_import = ast.imports.find { |import| import.path.parts == %w[std testing] }
|
|
403
|
-
unless testing_import
|
|
404
|
-
@err.puts("a test file must import std.testing: #{path}")
|
|
405
|
-
return 1
|
|
406
|
-
end
|
|
407
|
-
testing_alias = testing_import.alias_name || testing_import.path.parts.last
|
|
408
|
-
|
|
409
402
|
death_tests, normal_tests = tests.partition { |test| expect_fatal_attribute?(test) }
|
|
410
403
|
|
|
411
404
|
exit_code = 0
|
|
412
405
|
|
|
413
406
|
unless normal_tests.empty?
|
|
414
407
|
runner_source = source.dup
|
|
415
|
-
runner_source << "\n\n" << test_runner_main(
|
|
416
|
-
exit_code =
|
|
408
|
+
runner_source << "\n\n" << test_runner_main(normal_tests.map(&:name))
|
|
409
|
+
exit_code = run_normal_tests(path, runner_source, normal_tests.map(&:name), options:, locked:)
|
|
417
410
|
end
|
|
418
411
|
|
|
419
412
|
death_tests.each do |death_test|
|
|
@@ -459,30 +452,53 @@ module MilkTea
|
|
|
459
452
|
def death_test_runner_main(test_name)
|
|
460
453
|
[
|
|
461
454
|
"function main() -> int:",
|
|
462
|
-
"
|
|
463
|
-
"
|
|
464
|
-
" return 0",
|
|
465
|
-
" Result.failure:",
|
|
466
|
-
" return 0",
|
|
455
|
+
" #{test_name}()",
|
|
456
|
+
" return 0",
|
|
467
457
|
].join("\n") + "\n"
|
|
468
458
|
end
|
|
469
459
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
460
|
+
# The runner binary runs exactly one test per invocation, selected by
|
|
461
|
+
# `argv[1]`. This gives per-test isolation: an aborting assertion in one
|
|
462
|
+
# test cannot suppress the results of its siblings.
|
|
463
|
+
def test_runner_main(test_names)
|
|
464
|
+
lines = ["function main(args: span[str]) -> int:"]
|
|
465
|
+
lines << " let which = if args.len > 1: args[1] else: \"\""
|
|
466
|
+
lines << " match which:"
|
|
473
467
|
test_names.each do |name|
|
|
474
|
-
lines << "
|
|
468
|
+
lines << " \"#{name}\":"
|
|
469
|
+
lines << " #{name}()"
|
|
475
470
|
end
|
|
476
|
-
lines << "
|
|
471
|
+
lines << " _:"
|
|
472
|
+
lines << " return 2"
|
|
473
|
+
lines << " return 0"
|
|
477
474
|
lines.join("\n") + "\n"
|
|
478
475
|
end
|
|
479
476
|
|
|
480
|
-
def
|
|
477
|
+
def run_normal_tests(source_path, runner_source, test_names, options:, locked:)
|
|
481
478
|
with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
|
|
482
|
-
|
|
479
|
+
exit_code = 0
|
|
480
|
+
test_names.each do |name|
|
|
481
|
+
output, status, timed_out = spawn_sandboxed(binary_path, [name])
|
|
482
|
+
if timed_out
|
|
483
|
+
@out.puts("FAIL - #{name}: timed out")
|
|
484
|
+
exit_code = 1
|
|
485
|
+
elsif status&.exitstatus == 0
|
|
486
|
+
@out.puts("ok - #{name}")
|
|
487
|
+
else
|
|
488
|
+
@out.puts("FAIL - #{name}: #{first_fail_message(output)}")
|
|
489
|
+
exit_code = 1
|
|
490
|
+
end
|
|
491
|
+
@out.flush if @out.respond_to?(:flush)
|
|
492
|
+
end
|
|
493
|
+
exit_code
|
|
483
494
|
end
|
|
484
495
|
end
|
|
485
496
|
|
|
497
|
+
def first_fail_message(output)
|
|
498
|
+
line = output.to_s.lines.map(&:chomp).find { |entry| !entry.strip.empty? }
|
|
499
|
+
line.nil? || line.strip.empty? ? "test aborted" : line.strip
|
|
500
|
+
end
|
|
501
|
+
|
|
486
502
|
def with_synthesized_binary(source_path, runner_source, options:, locked:)
|
|
487
503
|
directory = File.dirname(File.expand_path(source_path))
|
|
488
504
|
runner_path = File.join(directory, "__mt_test_runner_#{Process.pid}.mt")
|
|
@@ -508,30 +524,13 @@ module MilkTea
|
|
|
508
524
|
end
|
|
509
525
|
end
|
|
510
526
|
|
|
511
|
-
def
|
|
512
|
-
output, status, timed_out = spawn_sandboxed(binary_path)
|
|
513
|
-
@out.write(output)
|
|
514
|
-
@out.flush if @out.respond_to?(:flush)
|
|
515
|
-
|
|
516
|
-
if timed_out
|
|
517
|
-
@err.puts("test run timed out after #{@test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS}s")
|
|
518
|
-
return 1
|
|
519
|
-
end
|
|
520
|
-
if status&.signaled?
|
|
521
|
-
@err.puts("test run crashed (signal #{status.termsig})")
|
|
522
|
-
return 1
|
|
523
|
-
end
|
|
524
|
-
|
|
525
|
-
status&.exitstatus || 1
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
def spawn_sandboxed(binary_path)
|
|
527
|
+
def spawn_sandboxed(binary_path, args = [])
|
|
529
528
|
timeout_seconds = @test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS
|
|
530
529
|
memory_bytes = @test_memory_bytes || TEST_RUN_MEMORY_LIMIT_BYTES
|
|
531
530
|
reader, writer = IO.pipe
|
|
532
531
|
spawn_options = { out: writer, err: writer, pgroup: true }
|
|
533
532
|
spawn_options[:rlimit_as] = memory_bytes unless @test_sanitize
|
|
534
|
-
pid = Process.spawn(binary_path, **spawn_options)
|
|
533
|
+
pid = Process.spawn(binary_path, *args, **spawn_options)
|
|
535
534
|
writer.close
|
|
536
535
|
|
|
537
536
|
status = nil
|
data/lib/milk_tea/tooling/cli.rb
CHANGED
|
@@ -597,10 +597,11 @@ module MilkTea
|
|
|
597
597
|
root), recursively discovers every .mt file that contains @[test] functions,
|
|
598
598
|
runs each as its own test binary, and prints an aggregate summary.
|
|
599
599
|
|
|
600
|
-
Functions annotated with @[test] must take no parameters and return
|
|
601
|
-
|
|
602
|
-
test
|
|
603
|
-
|
|
600
|
+
Functions annotated with @[test] must take no parameters and return void;
|
|
601
|
+
a failing test aborts via the `assert`/`expect`/`expect_eq`/`expect_ne`
|
|
602
|
+
intrinsics. `mtc test` synthesizes a runner that invokes each test in its
|
|
603
|
+
own process (for isolation) and reports the results. A test file must not
|
|
604
|
+
define `main`.
|
|
604
605
|
|
|
605
606
|
Each test binary runs under a wall-clock timeout and an address-space
|
|
606
607
|
memory cap so a hanging or runaway test cannot stall or exhaust the host.
|
|
@@ -105,14 +105,25 @@ module MilkTea
|
|
|
105
105
|
def terminating_expression?(expression)
|
|
106
106
|
case expression
|
|
107
107
|
when AST::Call
|
|
108
|
-
terminating_callee?(expression.callee) || static_assert_false?(expression)
|
|
108
|
+
terminating_callee?(expression.callee) || static_assert_false?(expression) || assertion_false?(expression)
|
|
109
109
|
when AST::Specialization
|
|
110
|
-
terminating_callee?(expression.callee) || static_assert_false?(expression)
|
|
110
|
+
terminating_callee?(expression.callee) || static_assert_false?(expression) || assertion_false?(expression)
|
|
111
111
|
else
|
|
112
112
|
false
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
def assertion_false?(expression)
|
|
117
|
+
return false unless expression.is_a?(AST::Call)
|
|
118
|
+
|
|
119
|
+
callee = expression.callee
|
|
120
|
+
return false unless callee.is_a?(AST::Identifier)
|
|
121
|
+
return false unless %w[assert expect].include?(callee.name)
|
|
122
|
+
|
|
123
|
+
first_arg = expression.arguments.first
|
|
124
|
+
first_arg.is_a?(AST::BooleanLiteral) && first_arg.value == false
|
|
125
|
+
end
|
|
126
|
+
|
|
116
127
|
def static_assert_false?(expression)
|
|
117
128
|
return false unless expression.callee.is_a?(AST::Identifier)
|
|
118
129
|
return false unless expression.callee.name == "static_assert"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mt-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Long (Teefan) Tran
|
|
@@ -150,6 +150,7 @@ files:
|
|
|
150
150
|
- docs/language-design.md
|
|
151
151
|
- docs/language-manual.md
|
|
152
152
|
- docs/lsp-performance.md
|
|
153
|
+
- docs/testing.md
|
|
153
154
|
- lib/milk_tea.rb
|
|
154
155
|
- lib/milk_tea/base.rb
|
|
155
156
|
- lib/milk_tea/bindings.rb
|
|
@@ -233,6 +234,7 @@ files:
|
|
|
233
234
|
- lib/milk_tea/core/lexer/trivia.rb
|
|
234
235
|
- lib/milk_tea/core/lowering.rb
|
|
235
236
|
- lib/milk_tea/core/lowering/artifacts.rb
|
|
237
|
+
- lib/milk_tea/core/lowering/assertions.rb
|
|
236
238
|
- lib/milk_tea/core/lowering/async/analysis.rb
|
|
237
239
|
- lib/milk_tea/core/lowering/async/async_lowering.rb
|
|
238
240
|
- lib/milk_tea/core/lowering/async/frame_builder.rb
|
|
@@ -609,7 +611,6 @@ files:
|
|
|
609
611
|
- std/sync.mt
|
|
610
612
|
- std/tar.mt
|
|
611
613
|
- std/terminal.mt
|
|
612
|
-
- std/testing.mt
|
|
613
614
|
- std/thread.mt
|
|
614
615
|
- std/time.mt
|
|
615
616
|
- std/timer.mt
|
|
@@ -629,7 +630,7 @@ metadata:
|
|
|
629
630
|
homepage_uri: https://teefan.github.io/mt-lang/
|
|
630
631
|
source_code_uri: https://github.com/teefan/mt-lang
|
|
631
632
|
post_install_message: |
|
|
632
|
-
Milk Tea 0.4.
|
|
633
|
+
Milk Tea 0.4.21 installed!
|
|
633
634
|
|
|
634
635
|
System requirements:
|
|
635
636
|
- A C compiler (gcc or clang) must be available on PATH
|