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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/docs/language-design.md +3 -4
  3. data/docs/language-manual.md +3 -3
  4. data/lib/milk_tea/base.rb +1 -1
  5. data/lib/milk_tea/core/{binding_types.rb → bindings.rb} +1 -1
  6. data/lib/milk_tea/core/c_backend/expressions.rb +34 -23
  7. data/lib/milk_tea/core/c_backend/feature_detection.rb +25 -45
  8. data/lib/milk_tea/core/c_backend/type_collectors.rb +18 -30
  9. data/lib/milk_tea/core/c_backend/type_declaration.rb +0 -6
  10. data/lib/milk_tea/core/c_backend.rb +63 -54
  11. data/lib/milk_tea/core/compile_time.rb +279 -74
  12. data/lib/milk_tea/core/control_flow/constant_propagation.rb +1 -1
  13. data/lib/milk_tea/core/{compatibility_helpers.rb → intrinsics.rb} +9 -2
  14. data/lib/milk_tea/core/lexer.rb +55 -48
  15. data/lib/milk_tea/core/lowering/block.rb +9 -0
  16. data/lib/milk_tea/core/lowering/calls.rb +2 -0
  17. data/lib/milk_tea/core/lowering/declarations.rb +1 -1
  18. data/lib/milk_tea/core/lowering/functions.rb +11 -8
  19. data/lib/milk_tea/core/lowering/resolve.rb +10 -3
  20. data/lib/milk_tea/core/lowering/scans.rb +13 -18
  21. data/lib/milk_tea/core/lowering.rb +18 -18
  22. data/lib/milk_tea/core/module_binder.rb +9 -10
  23. data/lib/milk_tea/core/module_loader.rb +64 -57
  24. data/lib/milk_tea/core/module_path_resolver.rb +1 -4
  25. data/lib/milk_tea/core/parser/declarations.rb +43 -19
  26. data/lib/milk_tea/core/parser/expressions.rb +4 -7
  27. data/lib/milk_tea/core/parser/statements.rb +5 -5
  28. data/lib/milk_tea/core/parser.rb +43 -17
  29. data/lib/milk_tea/core/semantic_analyzer/calls.rb +24 -31
  30. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +20 -23
  31. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +117 -85
  32. data/lib/milk_tea/core/semantic_analyzer/statements.rb +19 -41
  33. data/lib/milk_tea/core/semantic_analyzer.rb +57 -38
  34. data/lib/milk_tea/core.rb +6 -2
  35. data/lib/milk_tea/dap/lldb_dap_backend.rb +153 -0
  36. data/lib/milk_tea/dap/server/lldb_backend.rb +1 -1
  37. data/lib/milk_tea/dap.rb +1 -1
  38. data/lib/milk_tea/lsp/server/semantic_tokens.rb +6 -0
  39. data/lib/milk_tea/lsp.rb +1 -7
  40. data/lib/milk_tea/{bindings/cli.rb → tooling/bindgen_cli.rb} +1 -1
  41. data/lib/milk_tea/tooling/cli/commands/bindgen.rb +11 -0
  42. data/lib/milk_tea/tooling/cli/commands/build.rb +37 -0
  43. data/lib/milk_tea/tooling/cli/commands/cache.rb +46 -0
  44. data/lib/milk_tea/tooling/cli/commands/check.rb +116 -0
  45. data/lib/milk_tea/tooling/cli/commands/command_base.rb +8 -0
  46. data/lib/milk_tea/tooling/cli/commands/completions.rb +48 -0
  47. data/lib/milk_tea/tooling/cli/commands/dap.rb +58 -0
  48. data/lib/milk_tea/tooling/cli/commands/debug.rb +77 -0
  49. data/lib/milk_tea/tooling/cli/commands/deps.rb +17 -0
  50. data/lib/milk_tea/tooling/cli/commands/docs.rb +58 -0
  51. data/lib/milk_tea/tooling/cli/commands/emit_c.rb +64 -0
  52. data/lib/milk_tea/tooling/cli/commands/format.rb +199 -0
  53. data/lib/milk_tea/tooling/cli/commands/lex.rb +46 -0
  54. data/lib/milk_tea/tooling/cli/commands/lint.rb +248 -0
  55. data/lib/milk_tea/tooling/cli/commands/lower.rb +50 -0
  56. data/lib/milk_tea/tooling/cli/commands/lsp.rb +43 -0
  57. data/lib/milk_tea/tooling/cli/commands/new.rb +26 -0
  58. data/lib/milk_tea/tooling/cli/commands/parse.rb +51 -0
  59. data/lib/milk_tea/tooling/cli/commands/run.rb +99 -0
  60. data/lib/milk_tea/tooling/cli/commands/snapshot.rb +117 -0
  61. data/lib/milk_tea/tooling/cli/commands/test.rb +557 -0
  62. data/lib/milk_tea/tooling/cli/commands/toolchain.rb +16 -0
  63. data/lib/milk_tea/tooling/cli.rb +101 -1893
  64. data/lib/milk_tea/tooling.rb +1 -1
  65. metadata +29 -9
  66. data/lib/milk_tea/core/compile_time/const_eval.rb +0 -182
  67. data/lib/milk_tea/core/module_loader/errors.rb +0 -23
  68. data/lib/milk_tea/dap/backends/lldb_dap.rb +0 -156
  69. /data/lib/milk_tea/core/lowering/{async.rb → async/frame_builder.rb} +0 -0
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDap
6
+ def dap_command
7
+ preferred_backend_kind = "process"
8
+ adapter_command = nil
9
+
10
+ until @argv.empty?
11
+ arg = @argv.shift
12
+ case arg
13
+ when "--log-level"
14
+ @argv.shift
15
+ when /\A--log-level=(.+)\z/
16
+ # accept and ignore
17
+ when "--backend"
18
+ preferred_backend_kind = @argv.shift&.downcase
19
+ when /\A--backend=(.+)\z/
20
+ preferred_backend_kind = ::Regexp.last_match(1).downcase
21
+ when "--adapter-path"
22
+ adapter_path = @argv.shift
23
+ adapter_command = resolve_adapter_path(adapter_path)
24
+ return 1 unless adapter_command
25
+ when /\A--adapter-path=(.+)\z/
26
+ adapter_path = ::Regexp.last_match(1)
27
+ adapter_command = resolve_adapter_path(adapter_path)
28
+ return 1 unless adapter_command
29
+ else
30
+ if arg.start_with?("-")
31
+ @err.puts("dap: unknown option #{arg}")
32
+ return 1
33
+ end
34
+ @err.puts("dap: unexpected argument #{arg}")
35
+ return 1
36
+ end
37
+ end
38
+
39
+ require "milk_tea/dap/server" unless defined?(MilkTea::DAP::Server)
40
+ server = MilkTea::DAP::Server.new(
41
+ preferred_backend_kind:,
42
+ adapter_command:,
43
+ )
44
+ server.run
45
+ 0
46
+ end
47
+
48
+ def resolve_adapter_path(adapter_path)
49
+ unless adapter_path && File.file?(adapter_path)
50
+ @err.puts("dap: adapter path not found: #{adapter_path}")
51
+ return nil
52
+ end
53
+ expanded = File.expand_path(adapter_path)
54
+ adapter_path.end_with?(".rb") ? [RbConfig.ruby, expanded] : [expanded]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDebug
6
+ def debug_command
7
+ unless @argv.any?
8
+ @err.puts("missing source file path")
9
+ print_usage(@err)
10
+ return 1
11
+ end
12
+
13
+ resolution = extract_resolution_flags!
14
+ input_paths = @argv.dup
15
+ return 1 unless ensure_known_source_operands!("debug", input_paths)
16
+
17
+ path = expand_source_paths(input_paths).first
18
+ unless path
19
+ @err.puts("no .mt files found in #{input_paths.join(', ')}")
20
+ return 1
21
+ end
22
+
23
+ ensure_current_lockfile!(path) if resolution[:frozen]
24
+
25
+ source = read_source_file(path)
26
+ resolved_path = File.expand_path(path)
27
+
28
+ tokens = MilkTea::Lexer.lex(source, path: resolved_path)
29
+
30
+ parse_result = MilkTea::Parser.parse_collecting_errors(source, path: resolved_path)
31
+ ast = parse_result.ast
32
+ parse_errors = parse_result.errors.dup
33
+
34
+ facts = nil
35
+ snapshot = nil
36
+ loader_ast = ast
37
+
38
+ if ast && parse_errors.empty?
39
+ begin
40
+ loader = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform)
41
+ loader_ast = loader.load_file(resolved_path)
42
+
43
+ import_result = loader.send(:imported_modules_for_ast_collecting_errors, loader_ast, importer_path: resolved_path)
44
+ import_errors = import_result.respond_to?(:errors) ? import_result.errors : []
45
+ parse_errors.concat(import_errors) unless import_errors.empty?
46
+
47
+ snapshot = MilkTea::SemanticAnalyzer.tooling_snapshot(
48
+ loader_ast,
49
+ imported_modules: import_result.modules,
50
+ allow_missing_imports: true,
51
+ path: resolved_path,
52
+ )
53
+ facts = snapshot&.facts
54
+ rescue MilkTea::LexError, MilkTea::ParseError, ModuleLoadError, SemanticError => e
55
+ parse_errors << e
56
+ end
57
+ end
58
+
59
+ text = DebugInfoFormatter.format_all(
60
+ content: source,
61
+ tokens: tokens,
62
+ ast: loader_ast,
63
+ parse_errors: parse_errors,
64
+ facts: facts,
65
+ snapshot: snapshot,
66
+ path: resolved_path,
67
+ )
68
+
69
+ @out.puts(text)
70
+ 0
71
+ rescue MilkTea::LexError => e
72
+ @err.puts(ErrorFormatter.format(e, color: error_color?(@err)))
73
+ 1
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDeps
6
+ def deps_command
7
+ PackageManagerCLI.start(
8
+ @argv,
9
+ out: @out,
10
+ err: @err,
11
+ help_printer: method(:print_deps_help),
12
+ services: package_services,
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandDocs
6
+ def docs_command
7
+ port = nil
8
+ open_flag = false
9
+
10
+ while (arg = @argv.first)
11
+ case arg
12
+ when "--port", "-p"
13
+ @argv.shift
14
+ port = @argv.shift.to_i
15
+ port = nil if port <= 0 || port > 65535
16
+ when "--open", "-o"
17
+ open_flag = true
18
+ @argv.shift
19
+ else
20
+ break
21
+ end
22
+ end
23
+
24
+ port = resolve_docs_port(port)
25
+
26
+ DocsApp.set :port, port
27
+ DocsApp.set :bind, "127.0.0.1"
28
+ DocsApp.set :environment, :production
29
+ DocsApp.set :server, :puma
30
+
31
+ url = "http://127.0.0.1:#{port}/"
32
+
33
+ @out.puts("Serving Milk Tea docs at #{url}")
34
+ @out.puts("Press Ctrl+C to stop.")
35
+
36
+ if open_flag
37
+ open_browser(url)
38
+ end
39
+
40
+ DocsApp.run!
41
+ 0
42
+ rescue Interrupt
43
+ 0
44
+ end
45
+
46
+ def resolve_docs_port(preferred)
47
+ return preferred if preferred
48
+
49
+ server = TCPServer.new("127.0.0.1", 0)
50
+ port = server.addr[1]
51
+ server.close
52
+ port
53
+ rescue StandardError
54
+ 4567
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandEmitC
6
+ def emit_c_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!("emit-c", input_paths)
23
+
24
+ program_paths = input_paths.map { |p| resolve_program_path(p) }
25
+ return 1 if program_paths.include?(nil)
26
+
27
+ ensure_current_lockfiles!(input_paths) if resolution[:frozen]
28
+
29
+ multiple = program_paths.length > 1
30
+ program_paths.each_with_index do |path, index|
31
+ program = make_module_loader(path, locked: resolution[:locked], platform: ModuleLoader.default_host_platform).check_program(path)
32
+ if multiple
33
+ @out.puts("/* --- #{path} --- */")
34
+ end
35
+ @out.write(CBackend.generate_c(Lowering.lower(program), emit_line_directives: false))
36
+ @out.puts if multiple && index < program_paths.length - 1
37
+ end
38
+ 0
39
+ end
40
+
41
+ def resolve_program_path(path)
42
+ return path unless File.directory?(path)
43
+
44
+ manifest_path = File.join(path, "package.toml")
45
+ unless File.file?(manifest_path)
46
+ @err.puts("no package.toml found in #{path}")
47
+ return nil
48
+ end
49
+
50
+ manifest = PackageManifest.load(path)
51
+ entry_path = manifest.source_path
52
+ unless entry_path && File.file?(entry_path)
53
+ @err.puts("no build entry found for #{path}")
54
+ return nil
55
+ end
56
+
57
+ entry_path
58
+ rescue PackageManifestError => e
59
+ @err.puts("failed to load package manifest for #{path}: #{e.message}")
60
+ nil
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandFormat
6
+ def format_command
7
+ parsed = parse_format_options
8
+ return 1 unless parsed
9
+
10
+ options = parsed[:options]
11
+ input_paths = parsed[:input_paths]
12
+
13
+ if input_paths.empty?
14
+ @err.puts("missing source file path")
15
+ print_usage(@err)
16
+ return 1
17
+ end
18
+
19
+ paths = expand_source_paths(input_paths)
20
+ return 0 if print_no_source_files_if_empty(paths, input_paths)
21
+
22
+ multiple_sources = input_paths.length > 1 || input_paths.any? { |path| File.directory?(path) }
23
+ if multiple_sources
24
+ unless options[:check] || options[:write]
25
+ @err.puts("format on multiple sources requires --check or --write")
26
+ print_usage(@err)
27
+ return 1
28
+ end
29
+
30
+ return format_paths(paths, options)
31
+ end
32
+
33
+ path = paths.first
34
+
35
+ source = read_source_file(path)
36
+ format_profile = options[:profile] ? Linter::Profile.new : nil
37
+ start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
38
+ result = Formatter.check_source(source, path: path, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
39
+ elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
40
+
41
+ rc = if options[:check]
42
+ announce_file_action(path, "format-check")
43
+ if result.changed
44
+ info("needs formatting #{path}")
45
+ 1
46
+ else
47
+ info("already formatted #{path}")
48
+ 0
49
+ end
50
+ elsif options[:write]
51
+ announce_file_action(path, "format-write")
52
+ if result.changed
53
+ File.write(path, result.formatted_source)
54
+ info("formatted #{path}")
55
+ else
56
+ info("already formatted #{path}")
57
+ end
58
+ 0
59
+ else
60
+ @out.write(result.formatted_source)
61
+ 0
62
+ end
63
+
64
+ print_file_profiles([{ path:, total_ms: elapsed_ms, profile: format_profile }], "format") if options[:profile]
65
+ rc
66
+ end
67
+
68
+ def format_paths(paths, options)
69
+ format_profiles = []
70
+ if options[:check]
71
+ needs_fmt = []
72
+ paths.each do |p|
73
+ announce_file_action(p, "format-check")
74
+ format_profile = options[:profile] ? Linter::Profile.new : nil
75
+ start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
76
+ result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
77
+ elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
78
+ format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
79
+ needs_fmt << p if result.changed
80
+ end
81
+ print_file_profiles(format_profiles, "format") if options[:profile]
82
+ if needs_fmt.empty?
83
+ info("all #{paths.size} file(s) already formatted")
84
+ return 0
85
+ end
86
+ needs_fmt.each { |p| info("needs formatting #{p}") }
87
+ info("#{needs_fmt.size} file(s) need formatting")
88
+ return 1
89
+ end
90
+
91
+ # --write
92
+ changed = 0
93
+ paths.each do |p|
94
+ announce_file_action(p, "format-write")
95
+ format_profile = options[:profile] ? Linter::Profile.new : nil
96
+ start_time = options[:profile] ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
97
+ result = Formatter.check_source(read_source_file(p), path: p, mode: options[:mode], max_line_length: options[:max_line_length], profile: format_profile)
98
+ elapsed_ms = start_time ? ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0).round(1) : nil
99
+ format_profiles << { path: p, total_ms: elapsed_ms, profile: format_profile } if options[:profile]
100
+ if result.changed
101
+ File.write(p, result.formatted_source)
102
+ info("formatted #{p}")
103
+ changed += 1
104
+ end
105
+ end
106
+ print_file_profiles(format_profiles, "format") if options[:profile]
107
+ info("formatted #{changed} of #{paths.size} file(s)")
108
+ 0
109
+ end
110
+
111
+ def parse_format_options
112
+ options = {
113
+ check: false,
114
+ write: false,
115
+ mode: :safe,
116
+ max_line_length: nil,
117
+ profile: false,
118
+ }
119
+ input_paths = []
120
+
121
+ until @argv.empty?
122
+ option = @argv.shift
123
+ if option.start_with?("-")
124
+ case option
125
+ when "--check"
126
+ options[:check] = true
127
+ when "--write", "-w"
128
+ options[:write] = true
129
+ when "--preserve"
130
+ options[:mode] = :preserve
131
+ when "--canonical"
132
+ options[:mode] = :canonical
133
+ when "--safe"
134
+ options[:mode] = :safe
135
+ when "--tidy"
136
+ options[:mode] = :tidy
137
+ when "--max-line-length"
138
+ value = @argv.shift
139
+ return missing_option_value(option) unless value
140
+
141
+ line_length = Integer(value, exception: false)
142
+ unless line_length && line_length.positive?
143
+ @err.puts("--max-line-length must be a positive integer")
144
+ print_usage(@err)
145
+ return nil
146
+ end
147
+
148
+ options[:max_line_length] = line_length
149
+ when "--timings"
150
+ options[:profile] = true
151
+ when "--"
152
+ input_paths.concat(@argv)
153
+ @argv.clear
154
+ else
155
+ @err.puts("unknown format option #{option}")
156
+ print_usage(@err)
157
+ return nil
158
+ end
159
+ else
160
+ input_paths << option
161
+ end
162
+ end
163
+
164
+ if options[:check] && options[:write]
165
+ @err.puts("format options --check and --write cannot be combined")
166
+ print_usage(@err)
167
+ return nil
168
+ end
169
+
170
+ { options:, input_paths: }
171
+ end
172
+
173
+ def print_file_profiles(file_profiles, label)
174
+ sorted = file_profiles.sort_by { |fp| -fp[:total_ms] }
175
+ return if sorted.empty?
176
+
177
+ @out.puts
178
+ if sorted.size == 1
179
+ entry = sorted.first
180
+ phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
181
+ phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 0.1 }&.join(", ")
182
+ detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
183
+ @out.puts("#{label} profile #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
184
+ return
185
+ end
186
+
187
+ @out.puts("Profile (#{label}): #{sorted.size} file(s)")
188
+ sorted.each do |entry|
189
+ phases = entry[:profile]&.timings_ms&.sort_by { |_, ms| -ms }
190
+ phase_str = phases&.filter_map { |name, ms| "#{name}: #{format('%.1f', ms)}ms" if ms >= 1.0 }&.join(", ")
191
+ detail = phase_str && !phase_str.empty? ? " (#{phase_str})" : ""
192
+ @out.puts(" #{entry[:path]}: #{format('%.1f', entry[:total_ms])}ms#{detail}")
193
+ end
194
+ total = sorted.sum { |fp| fp[:total_ms] }
195
+ @out.puts("Total: #{format('%.1f', total)}ms")
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ class CLI
5
+ module CommandLex
6
+ def lex_command
7
+ path = nil
8
+ sexpr = false
9
+
10
+ args = @argv.dup
11
+ @argv = []
12
+ until args.empty?
13
+ arg = args.shift
14
+ next if arg == "--"
15
+
16
+ if arg == "--sexpr"
17
+ sexpr = true
18
+ next
19
+ end
20
+
21
+ if path.nil?
22
+ path = arg
23
+ else
24
+ @err.puts("unknown option: #{arg}")
25
+ print_usage(@err)
26
+ return 1
27
+ end
28
+ end
29
+
30
+ unless path
31
+ @err.puts("missing source file path")
32
+ print_usage(@err)
33
+ return 1
34
+ end
35
+
36
+ tokens = Lexer.lex(read_source_file(path), path: path)
37
+ if sexpr
38
+ @out.puts(SexprDumper.dump_tokens(tokens))
39
+ else
40
+ @out.write(PP.pp(tokens, +""))
41
+ end
42
+ 0
43
+ end
44
+ end
45
+ end
46
+ end