mt-lang 0.3.17 → 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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/lib/milk_tea/base.rb +1 -1
  3. data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
  4. data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
  5. data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
  6. data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
  7. data/lib/milk_tea/core/c_backend.rb +49 -39
  8. data/lib/milk_tea/core/compile_time.rb +98 -72
  9. data/lib/milk_tea/core/intrinsics.rb +7 -0
  10. data/lib/milk_tea/core/lexer.rb +46 -35
  11. data/lib/milk_tea/core/lowering/block.rb +9 -0
  12. data/lib/milk_tea/core/lowering/calls.rb +2 -0
  13. data/lib/milk_tea/core/lowering/declarations.rb +1 -1
  14. data/lib/milk_tea/core/lowering/functions.rb +11 -8
  15. data/lib/milk_tea/core/lowering/resolve.rb +10 -3
  16. data/lib/milk_tea/core/lowering/scans.rb +13 -18
  17. data/lib/milk_tea/core/module_binder.rb +9 -10
  18. data/lib/milk_tea/core/module_loader.rb +38 -42
  19. data/lib/milk_tea/core/module_path_resolver.rb +1 -4
  20. data/lib/milk_tea/core/parser/declarations.rb +43 -19
  21. data/lib/milk_tea/core/parser/expressions.rb +4 -7
  22. data/lib/milk_tea/core/parser/statements.rb +5 -5
  23. data/lib/milk_tea/core/parser.rb +26 -0
  24. data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
  25. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
  26. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +117 -85
  27. data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
  28. data/lib/milk_tea/core/semantic_analyzer.rb +56 -37
  29. data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
  30. data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
  31. data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
  32. data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
  33. data/lib/milk_tea/tooling/cli/commands/check.rb +116 -0
  34. data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
  35. data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
  36. data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
  37. data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
  38. data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
  39. data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
  40. data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
  41. data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
  42. data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
  43. data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
  44. data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
  45. data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
  46. data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
  47. data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
  48. data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
  49. data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
  50. data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
  51. data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
  52. data/lib/milk_tea/tooling/cli.rb +101 -1893
  53. metadata +24 -2
@@ -0,0 +1,557 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandTest
6
+ TEST_RUN_TIMEOUT_SECONDS = 30
7
+ TEST_RUN_MEMORY_LIMIT_BYTES = 1024 * 1024 * 1024
8
+
9
+ TestResult = Data.define(:name, :status, :detail)
10
+
11
+ def test_command
12
+ limits = extract_test_limit_flags!
13
+ return 1 unless limits
14
+
15
+ @test_timeout_seconds, @test_memory_bytes, @test_jobs, @test_sanitize, @test_filter, @test_format = limits
16
+
17
+ path, options = extract_path_and_options
18
+ return 1 unless path
19
+
20
+ frozen = options.delete(:frozen)
21
+ ensure_current_lockfile!(path) if frozen
22
+ locked = options.delete(:locked)
23
+
24
+ return dispatch_test_run(path, options:, locked:) if @test_format == :human
25
+
26
+ buffer = StringIO.new
27
+ real_out = @out
28
+ real_err = @err
29
+ @out = buffer
30
+ @err = buffer
31
+ begin
32
+ exit_code = dispatch_test_run(path, options:, locked:)
33
+ ensure
34
+ @out = real_out
35
+ @err = real_err
36
+ end
37
+
38
+ emit_machine_results(parse_test_output(buffer.string), @test_format, real_out)
39
+ exit_code
40
+ end
41
+
42
+ def dispatch_test_run(path, options:, locked:)
43
+ if File.directory?(path)
44
+ run_test_directory(path, options:, locked:)
45
+ elsif File.file?(path)
46
+ if compile_fail_fixture?(File.read(path))
47
+ run_compile_fail_test(path) ? 0 : 1
48
+ else
49
+ run_test_file(path, options:, locked:)
50
+ end
51
+ else
52
+ @err.puts("mtc test: not a file or directory: #{path}")
53
+ 1
54
+ end
55
+ end
56
+
57
+ def extract_test_limit_flags!
58
+ timeout_seconds = TEST_RUN_TIMEOUT_SECONDS
59
+ memory_bytes = TEST_RUN_MEMORY_LIMIT_BYTES
60
+ jobs = 1
61
+ sanitize = false
62
+ filter = nil
63
+ format = :human
64
+ remaining = []
65
+ until @argv.empty?
66
+ arg = @argv.shift
67
+ case arg
68
+ when "--timeout"
69
+ value = @argv.shift
70
+ seconds = value && Integer(value, exception: false)
71
+ unless seconds&.positive?
72
+ @err.puts("--timeout requires a positive integer (seconds)")
73
+ return nil
74
+ end
75
+ timeout_seconds = seconds
76
+ when "--mem"
77
+ value = @argv.shift
78
+ megabytes = value && Integer(value, exception: false)
79
+ unless megabytes&.positive?
80
+ @err.puts("--mem requires a positive integer (megabytes)")
81
+ return nil
82
+ end
83
+ memory_bytes = megabytes * 1024 * 1024
84
+ when "--jobs"
85
+ value = @argv.shift
86
+ count = value && Integer(value, exception: false)
87
+ unless count&.positive?
88
+ @err.puts("--jobs requires a positive integer")
89
+ return nil
90
+ end
91
+ jobs = count
92
+ when "--sanitize"
93
+ sanitize = true
94
+ when "-n", "--name"
95
+ value = @argv.shift
96
+ unless value
97
+ @err.puts("-n requires a name substring")
98
+ return nil
99
+ end
100
+ filter = value
101
+ when "--format"
102
+ value = @argv.shift
103
+ unless %w[human tap junit].include?(value)
104
+ @err.puts("--format must be human, tap, or junit")
105
+ return nil
106
+ end
107
+ format = value.to_sym
108
+ when "--"
109
+ remaining << arg
110
+ remaining.concat(@argv)
111
+ @argv.clear
112
+ else
113
+ remaining << arg
114
+ end
115
+ end
116
+ @argv.replace(remaining)
117
+ [timeout_seconds, memory_bytes, jobs, sanitize, filter, format]
118
+ end
119
+
120
+ def parse_test_output(text)
121
+ results = []
122
+ current_file = nil
123
+ text.each_line do |raw|
124
+ line = raw.chomp
125
+ case line
126
+ when /\A# (.+)\z/
127
+ current_file = ::Regexp.last_match(1)
128
+ when /\Aok - (.+)\z/
129
+ results << TestResult.new(name: ::Regexp.last_match(1), status: :pass, detail: nil)
130
+ when /\Askip - (.+?)(?:: (.*))?\z/
131
+ results << TestResult.new(name: ::Regexp.last_match(1), status: :skip, detail: ::Regexp.last_match(2))
132
+ when /\AFAIL - (.+?)(?:: (.*))?\z/
133
+ results << TestResult.new(name: ::Regexp.last_match(1), status: :fail, detail: ::Regexp.last_match(2))
134
+ when /\AFAILED - (.+?) \(build error\)\z/
135
+ results << TestResult.new(name: "#{::Regexp.last_match(1)} (build error)", status: :fail, detail: "build error")
136
+ when /\Atest run (?:timed out|crashed)/, /\ASUMMARY: \w+Sanitizer/
137
+ results << TestResult.new(name: "#{current_file || 'test'} (#{line})", status: :fail, detail: line)
138
+ end
139
+ end
140
+ results
141
+ end
142
+
143
+ def emit_machine_results(results, format, out)
144
+ case format
145
+ when :tap then emit_tap(results, out)
146
+ when :junit then emit_junit(results, out)
147
+ end
148
+ end
149
+
150
+ def emit_tap(results, out)
151
+ out.puts("TAP version 13")
152
+ out.puts("1..#{results.length}")
153
+ results.each_with_index do |result, index|
154
+ number = index + 1
155
+ case result.status
156
+ when :pass
157
+ out.puts("ok #{number} - #{result.name}")
158
+ when :skip
159
+ out.puts("ok #{number} - #{result.name} # SKIP#{result.detail ? " #{result.detail}" : ''}")
160
+ when :fail
161
+ out.puts("not ok #{number} - #{result.name}")
162
+ next unless result.detail
163
+
164
+ out.puts(" ---")
165
+ out.puts(" message: #{result.detail}")
166
+ out.puts(" ...")
167
+ end
168
+ end
169
+ end
170
+
171
+ def emit_junit(results, out)
172
+ failures = results.count { |result| result.status == :fail }
173
+ skipped = results.count { |result| result.status == :skip }
174
+ out.puts(%(<?xml version="1.0" encoding="UTF-8"?>))
175
+ out.puts(%(<testsuites tests="#{results.length}" failures="#{failures}" skipped="#{skipped}">))
176
+ out.puts(%( <testsuite name="mtc test" tests="#{results.length}" failures="#{failures}" skipped="#{skipped}">))
177
+ results.each do |result|
178
+ name = xml_escape(result.name)
179
+ case result.status
180
+ when :pass
181
+ out.puts(%( <testcase name="#{name}"/>))
182
+ when :skip
183
+ out.puts(%( <testcase name="#{name}"><skipped/></testcase>))
184
+ when :fail
185
+ out.puts(%( <testcase name="#{name}"><failure message="#{xml_escape(result.detail || 'failed')}"/></testcase>))
186
+ end
187
+ end
188
+ out.puts(" </testsuite>")
189
+ out.puts("</testsuites>")
190
+ end
191
+
192
+ def xml_escape(value)
193
+ value.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
194
+ end
195
+
196
+ def run_test_directory(directory, options:, locked:)
197
+ test_files = discover_test_files(directory)
198
+ if test_files.empty?
199
+ if @test_filter
200
+ @out.puts("no tests matched -n '#{@test_filter}' under #{directory}")
201
+ else
202
+ @out.puts("no @[test] functions or # expect-error: fixtures found under #{directory}")
203
+ end
204
+ return 0
205
+ end
206
+
207
+ jobs = @test_jobs || 1
208
+ return run_test_files_parallel(test_files, jobs:, options:, locked:) if jobs > 1 && Process.respond_to?(:fork)
209
+
210
+ failed = 0
211
+ test_files.each do |file, kind|
212
+ @out.puts("# #{file}")
213
+ @out.flush if @out.respond_to?(:flush)
214
+ failed += 1 unless run_classified_file(file, kind, options:, locked:).zero?
215
+ end
216
+
217
+ @out.puts("")
218
+ @out.puts("#{test_files.length} test file(s), #{failed} failed")
219
+ failed.zero? ? 0 : 1
220
+ end
221
+
222
+ def run_classified_file(file, kind, options:, locked:)
223
+ if kind == :compile_fail
224
+ run_compile_fail_test(file) ? 0 : 1
225
+ else
226
+ run_test_file_guarded(file, options:, locked:)
227
+ end
228
+ end
229
+
230
+ def run_compile_fail_test(path)
231
+ source = File.read(path)
232
+ expectations = extract_expect_error_directives(source)
233
+ messages = compile_fail_diagnostics(path)
234
+
235
+ if messages.empty?
236
+ @out.puts("FAIL - #{path} (compile-fail): expected a compile error, but it compiled cleanly")
237
+ @out.flush if @out.respond_to?(:flush)
238
+ return false
239
+ end
240
+
241
+ unmatched = expectations.find { |expected| messages.none? { |message| message.include?(expected) } }
242
+ if unmatched
243
+ @out.puts("FAIL - #{path} (compile-fail): no diagnostic matched #{unmatched.inspect}")
244
+ @out.flush if @out.respond_to?(:flush)
245
+ return false
246
+ end
247
+
248
+ @out.puts("ok - #{path} (compile-fail)")
249
+ @out.flush if @out.respond_to?(:flush)
250
+ true
251
+ end
252
+
253
+ def extract_expect_error_directives(source)
254
+ source.each_line.filter_map do |line|
255
+ match = line.match(/^\s*#\s*expect-error:\s*(.+?)\s*$/)
256
+ match && match[1]
257
+ end
258
+ end
259
+
260
+ def compile_fail_diagnostics(path)
261
+ errors, = check_single_reporting_all(path, locked: false)
262
+ errors
263
+ .select { |diagnostic| !diagnostic.respond_to?(:severity) || diagnostic.severity == :error }
264
+ .map { |diagnostic| ErrorFormatter.format(diagnostic, color: false) }
265
+ rescue StandardError => e
266
+ raise unless handled_cli_error?(e)
267
+
268
+ [ErrorFormatter.format(e, color: false)]
269
+ end
270
+
271
+ def run_test_file_guarded(file, options:, locked:)
272
+ run_test_file(file, options:, locked:)
273
+ rescue StandardError => e
274
+ raise unless handled_cli_error?(e)
275
+
276
+ @err.puts("FAILED - #{file} (build error)")
277
+ @err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
278
+ 1
279
+ end
280
+
281
+ def run_test_files_parallel(test_files, jobs:, options:, locked:)
282
+ results = Array.new(test_files.length)
283
+ result_paths = {}
284
+ active = {}
285
+ cursor = 0
286
+
287
+ while cursor < test_files.length || !active.empty?
288
+ while active.size < jobs && cursor < test_files.length
289
+ index = cursor
290
+ cursor += 1
291
+ result_path = File.join(Dir.tmpdir, "mttest_result_#{Process.pid}_#{index}")
292
+ result_paths[index] = result_path
293
+ pid = fork do
294
+ captured = StringIO.new
295
+ @out = captured
296
+ @err = captured
297
+ file, kind = test_files[index]
298
+ code = run_classified_file(file, kind, options:, locked:)
299
+ File.binwrite(result_path, [code].pack("N") + captured.string)
300
+ exit!(0)
301
+ end
302
+ active[pid] = index
303
+ end
304
+
305
+ finished_pid, = Process.wait2
306
+ index = active.delete(finished_pid)
307
+ next unless index
308
+
309
+ path = result_paths[index]
310
+ data = begin
311
+ File.binread(path)
312
+ rescue StandardError
313
+ (+"").b
314
+ end
315
+ File.delete(path) if File.exist?(path)
316
+ results[index] =
317
+ if data.bytesize >= 4
318
+ [data[0, 4].unpack1("N"), data.byteslice(4..).force_encoding(Encoding::UTF_8)]
319
+ else
320
+ [1, +""]
321
+ end
322
+ end
323
+
324
+ failed = 0
325
+ test_files.each_with_index do |(file, _kind), index|
326
+ code, output = results[index]
327
+ @out.puts("# #{file}")
328
+ @out.write(output.to_s)
329
+ failed += 1 unless code&.zero?
330
+ end
331
+ @out.flush if @out.respond_to?(:flush)
332
+ @out.puts("")
333
+ @out.puts("#{test_files.length} test file(s), #{failed} failed")
334
+ failed.zero? ? 0 : 1
335
+ end
336
+
337
+ def discover_test_files(directory)
338
+ Dir.glob(File.join(directory, "**", "*.mt")).sort.filter_map do |file|
339
+ next if File.basename(file).start_with?("__mt_test_runner_")
340
+
341
+ kind = classify_test_file(file)
342
+ kind && [file, kind]
343
+ end
344
+ end
345
+
346
+ def classify_test_file(file)
347
+ source = File.read(file)
348
+ if compile_fail_fixture?(source)
349
+ return nil if @test_filter && !File.basename(file, ".mt").include?(@test_filter)
350
+
351
+ return :compile_fail
352
+ end
353
+
354
+ ast = begin
355
+ MilkTea::Parser.parse(source, path: file)
356
+ rescue ParseError
357
+ return nil
358
+ end
359
+ has_match = ast.declarations.any? do |decl|
360
+ decl.is_a?(AST::FunctionDef) && test_attribute?(decl) && matches_filter?(decl.name)
361
+ end
362
+ has_match ? :test : nil
363
+ end
364
+
365
+ def compile_fail_fixture?(source)
366
+ source.match?(/^\s*#\s*expect-error:/)
367
+ end
368
+
369
+ def test_attribute?(decl)
370
+ decl.attributes.any? { |attribute| attribute.name.parts == ["test"] }
371
+ end
372
+
373
+ def matches_filter?(name)
374
+ @test_filter.nil? || name.include?(@test_filter)
375
+ end
376
+
377
+ def run_test_file(path, options:, locked:)
378
+ source = File.read(path)
379
+ ast = MilkTea::Parser.parse(source, path:)
380
+
381
+ if ast.declarations.any? { |decl| decl.is_a?(AST::FunctionDef) && decl.name == "main" }
382
+ @err.puts("a test file must not define 'main': #{path}")
383
+ return 1
384
+ end
385
+
386
+ tests = ast.declarations.select { |decl| decl.is_a?(AST::FunctionDef) && test_attribute?(decl) }
387
+
388
+ if tests.empty?
389
+ @out.puts("no @[test] functions found in #{path}")
390
+ return 0
391
+ end
392
+
393
+ tests = tests.select { |test| matches_filter?(test.name) }
394
+ return 0 if tests.empty?
395
+
396
+ invalid = tests.find { |test| !test.params.empty? }
397
+ if invalid
398
+ @err.puts("@[test] function '#{invalid.name}' must take no parameters")
399
+ return 1
400
+ end
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
+ death_tests, normal_tests = tests.partition { |test| expect_fatal_attribute?(test) }
410
+
411
+ exit_code = 0
412
+
413
+ unless normal_tests.empty?
414
+ runner_source = source.dup
415
+ runner_source << "\n\n" << test_runner_main(testing_alias, normal_tests.map(&:name))
416
+ exit_code = run_synthesized_tests(path, runner_source, options:, locked:)
417
+ end
418
+
419
+ death_tests.each do |death_test|
420
+ exit_code = 1 unless run_death_test(path, source, death_test.name, options:, locked:)
421
+ end
422
+
423
+ exit_code
424
+ end
425
+
426
+ def expect_fatal_attribute?(decl)
427
+ decl.attributes.any? { |attribute| attribute.name.parts == ["expect_fatal"] }
428
+ end
429
+
430
+ def run_death_test(source_path, source, test_name, options:, locked:)
431
+ runner_source = source.dup
432
+ runner_source << "\n\n" << death_test_runner_main(test_name)
433
+ classification = with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
434
+ classify_death_test(binary_path)
435
+ end
436
+
437
+ passed = classification == :aborted
438
+ line =
439
+ if passed
440
+ "ok - #{test_name} (expect_fatal)"
441
+ elsif classification == :timed_out
442
+ "FAIL - #{test_name} (expect_fatal): timed out"
443
+ else
444
+ "FAIL - #{test_name} (expect_fatal): expected a fatal abort, but the test returned"
445
+ end
446
+ @out.puts(line)
447
+ @out.flush if @out.respond_to?(:flush)
448
+ passed
449
+ end
450
+
451
+ def classify_death_test(binary_path)
452
+ _output, status, timed_out = spawn_sandboxed(binary_path)
453
+ return :timed_out if timed_out
454
+ return :returned if status&.exited? && status.exitstatus&.zero?
455
+
456
+ :aborted
457
+ end
458
+
459
+ def death_test_runner_main(test_name)
460
+ [
461
+ "function main() -> int:",
462
+ " match #{test_name}():",
463
+ " Result.success:",
464
+ " return 0",
465
+ " Result.failure:",
466
+ " return 0",
467
+ ].join("\n") + "\n"
468
+ end
469
+
470
+ def test_runner_main(testing_alias, test_names)
471
+ lines = ["function main() -> int:"]
472
+ lines << " var __mt_test_stats = #{testing_alias}.Stats.create()"
473
+ test_names.each do |name|
474
+ lines << " __mt_test_stats = #{testing_alias}.record(__mt_test_stats, #{name.inspect}, #{name}())"
475
+ end
476
+ lines << " return #{testing_alias}.summarize(__mt_test_stats)"
477
+ lines.join("\n") + "\n"
478
+ end
479
+
480
+ def run_synthesized_tests(source_path, runner_source, options:, locked:)
481
+ with_synthesized_binary(source_path, runner_source, options:, locked:) do |binary_path|
482
+ run_test_binary(binary_path)
483
+ end
484
+ end
485
+
486
+ def with_synthesized_binary(source_path, runner_source, options:, locked:)
487
+ directory = File.dirname(File.expand_path(source_path))
488
+ runner_path = File.join(directory, "__mt_test_runner_#{Process.pid}.mt")
489
+ binary_path = File.join(Dir.tmpdir, "__mt_test_runner_#{Process.pid}")
490
+
491
+ File.write(runner_path, runner_source)
492
+ begin
493
+ build_opts = options.except(:timings, :output_path, :bundle, :archive)
494
+ build_opts[:debug_guards] = false
495
+ Build.build(
496
+ runner_path,
497
+ output_path: binary_path,
498
+ module_roots: module_roots_for(source_path, locked:),
499
+ package_graph: package_graph_for(source_path, locked:),
500
+ frontend: @build_frontend,
501
+ sanitize: @test_sanitize,
502
+ **build_opts,
503
+ )
504
+ yield binary_path
505
+ ensure
506
+ File.delete(runner_path) if File.exist?(runner_path)
507
+ File.delete(binary_path) if File.exist?(binary_path)
508
+ end
509
+ end
510
+
511
+ def run_test_binary(binary_path)
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)
529
+ timeout_seconds = @test_timeout_seconds || TEST_RUN_TIMEOUT_SECONDS
530
+ memory_bytes = @test_memory_bytes || TEST_RUN_MEMORY_LIMIT_BYTES
531
+ reader, writer = IO.pipe
532
+ spawn_options = { out: writer, err: writer, pgroup: true }
533
+ spawn_options[:rlimit_as] = memory_bytes unless @test_sanitize
534
+ pid = Process.spawn(binary_path, **spawn_options)
535
+ writer.close
536
+
537
+ status = nil
538
+ timed_out = false
539
+ begin
540
+ Timeout.timeout(timeout_seconds) { _, status = Process.wait2(pid) }
541
+ rescue Timeout::Error
542
+ timed_out = true
543
+ begin
544
+ Process.kill("-KILL", Process.getpgid(pid))
545
+ Process.wait(pid)
546
+ rescue StandardError
547
+ nil
548
+ end
549
+ end
550
+
551
+ output = reader.read
552
+ reader.close
553
+ [output, status, timed_out]
554
+ end
555
+ end
556
+ end
557
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandToolchain
6
+ def toolchain_command
7
+ ToolchainCLI.start(
8
+ @argv,
9
+ out: @out,
10
+ err: @err,
11
+ help_printer: method(:print_toolchain_help),
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end